Skip to content

Commit

Permalink
Add a command to serve sites using the Dockerized nginx server (#1476)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra authored May 8, 2024
1 parent 625ac75 commit b99abf6
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 23 deletions.
12 changes: 7 additions & 5 deletions betty/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@
from betty.asyncio import wait_to_thread
from betty.contextlib import SynchronizedContextManager
from betty.error import UserFacingError
from betty.extension import demo
from betty.gui import BettyApplication
from betty.gui.app import WelcomeWindow
from betty.gui.project import ProjectWindow
from betty.locale import update_translations, init_translation, Str
from betty.logging import CliHandler
from betty.serde.load import AssertionFailed
Expand Down Expand Up @@ -268,7 +264,9 @@ async def _clear_caches(app: App) -> None:
@click.command(help="Explore a demonstration site.")
@app_command
async def _demo(app: App) -> None:
async with demo.DemoServer(app=app) as server:
from betty.extension.demo import DemoServer

async with DemoServer(app=app) as server:
await server.show()
while True:
await asyncio.sleep(999)
Expand All @@ -288,6 +286,10 @@ async def _demo(app: App) -> None:
@global_command
async def _gui(configuration_file_path: Path | None) -> None:
async with App.new_from_environment() as app:
from betty.gui import BettyApplication
from betty.gui.app import WelcomeWindow
from betty.gui.project import ProjectWindow

async with BettyApplication([sys.argv[0]]).with_app(app) as qapp:
window: QMainWindow
if configuration_file_path is None:
Expand Down
16 changes: 15 additions & 1 deletion betty/extension/nginx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
from collections.abc import Sequence
from pathlib import Path

from click import Command

from betty.app.extension import ConfigurableExtension
from betty.cli import CommandProvider
from betty.extension.nginx.artifact import (
generate_configuration_file,
generate_dockerfile_file,
)
from betty.extension.nginx.cli import _serve
from betty.extension.nginx.config import NginxConfiguration
from betty.extension.nginx.gui import _NginxGuiWidget
from betty.generate import Generator, GenerationContext
Expand All @@ -17,7 +21,11 @@


class Nginx(
ConfigurableExtension[NginxConfiguration], Generator, ServerProvider, GuiBuilder
ConfigurableExtension[NginxConfiguration],
Generator,
ServerProvider,
GuiBuilder,
CommandProvider,
):
@classmethod
def label(cls) -> Str:
Expand Down Expand Up @@ -63,3 +71,9 @@ def www_directory_path(self) -> str:

def gui_build(self) -> _NginxGuiWidget:
return _NginxGuiWidget(self._app, self._configuration)

@property
def commands(self) -> dict[str, Command]:
return {
"serve-nginx-docker": _serve,
}
20 changes: 20 additions & 0 deletions betty/extension/nginx/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Provide Command Line Interface functionality.
"""

from asyncio import sleep

import click

from betty.app import App
from betty.cli import app_command
from betty.extension.nginx import serve


@click.command(help="Serve a generated site with nginx in a Docker container.")
@app_command
async def _serve(app: App) -> None:
async with serve.DockerizedNginxServer(app) as server:
await server.show()
while True:
await sleep(999)
28 changes: 28 additions & 0 deletions betty/tests/extension/nginx/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from aiofiles.os import makedirs
from pytest_mock import MockerFixture

from betty.app import App
from betty.extension import Nginx
from betty.extension.nginx.serve import DockerizedNginxServer
from betty.tests.test_cli import run


class KeyboardInterruptedDockerizedNginxServer(DockerizedNginxServer):
async def start(self) -> None:
raise KeyboardInterrupt()


class TestServe:
async def test(self, mocker: MockerFixture, new_temporary_app: App) -> None:
mocker.patch(
"betty.extension.nginx.serve.DockerizedNginxServer",
new=KeyboardInterruptedDockerizedNginxServer,
)
new_temporary_app.project.configuration.extensions.enable(Nginx)
await new_temporary_app.project.configuration.write()
await makedirs(new_temporary_app.project.configuration.www_directory_path)
run(
"-c",
str(new_temporary_app.project.configuration.configuration_file_path),
"serve-nginx-docker",
)
32 changes: 16 additions & 16 deletions betty/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def commands(self) -> dict[str, Command]:
}


def _run(
def run(
*args: str,
expected_exit_code: int = 0,
) -> Result:
Expand All @@ -65,8 +65,8 @@ def _run(
The Betty command `{" ".join(args)}` unexpectedly exited with code {result.exit_code}, but {expected_exit_code} was expected.
Stdout:
{result.stdout}
Stdout:
{result.stdout}
Stderr:
{result.stderr}
"""
)
return result
Expand All @@ -85,14 +85,14 @@ async def new_temporary_app(mocker: MockerFixture) -> AsyncIterator[App]:

class TestMain:
async def test_without_arguments(self, new_temporary_app: App) -> None:
_run()
run()

async def test_help_without_configuration(self, new_temporary_app: App) -> None:
_run("--help")
run("--help")

async def test_configuration_without_help(self, new_temporary_app: App) -> None:
await new_temporary_app.project.configuration.write()
_run(
run(
"-c",
str(new_temporary_app.project.configuration.configuration_file_path),
expected_exit_code=2,
Expand All @@ -104,7 +104,7 @@ async def test_help_with_configuration(self, new_temporary_app: App) -> None:
)
await new_temporary_app.project.configuration.write()

_run(
run(
"-c",
str(new_temporary_app.project.configuration.configuration_file_path),
"--help",
Expand All @@ -117,7 +117,7 @@ async def test_help_with_invalid_configuration_file_path(
working_directory_path = Path(working_directory_path_str)
configuration_file_path = working_directory_path / "non-existent-betty.json"

_run("-c", str(configuration_file_path), "--help", expected_exit_code=1)
run("-c", str(configuration_file_path), "--help", expected_exit_code=1)

async def test_help_with_invalid_configuration(
self, new_temporary_app: App
Expand All @@ -129,7 +129,7 @@ async def test_help_with_invalid_configuration(
async with aiofiles.open(configuration_file_path, "w") as f:
await f.write(json.dumps(dump))

_run("-c", str(configuration_file_path), "--help", expected_exit_code=1)
run("-c", str(configuration_file_path), "--help", expected_exit_code=1)

async def test_with_discovered_configuration(self, new_temporary_app: App) -> None:
async with TemporaryDirectory() as working_directory_path_str:
Expand All @@ -146,7 +146,7 @@ async def test_with_discovered_configuration(self, new_temporary_app: App) -> No
}
await config_file.write(json.dumps(dump))
with chdir(working_directory_path):
_run("test", expected_exit_code=1)
run("test", expected_exit_code=1)


class TestCatchExceptions:
Expand All @@ -168,15 +168,15 @@ async def test_logging_uncaught_exception(self, caplog: LogCaptureFixture) -> No

class TestVersion:
async def test(self, new_temporary_app: App) -> None:
result = _run("--version")
result = run("--version")
assert "Betty" in result.stdout


class TestClearCaches:
async def test(self, new_temporary_app: App) -> None:
async with new_temporary_app:
await new_temporary_app.cache.set("KeepMeAroundPlease", "")
_run("clear-caches")
run("clear-caches")
async with new_temporary_app:
async with new_temporary_app.cache.get("KeepMeAroundPlease") as cache_item:
assert cache_item is None
Expand All @@ -193,7 +193,7 @@ async def test(self, mocker: MockerFixture, new_temporary_app: App) -> None:
"betty.extension.demo.DemoServer", new=KeyboardInterruptedDemoServer
)

_run("demo")
run("demo")


class KeyboardInterruptedDocumentationServer(DocumentationServer):
Expand All @@ -208,7 +208,7 @@ async def test(self, mocker: MockerFixture, new_temporary_app: App) -> None:
new=KeyboardInterruptedDocumentationServer,
)

_run("docs")
run("docs")


class TestGenerate:
Expand All @@ -217,7 +217,7 @@ async def test(self, mocker: MockerFixture, new_temporary_app: App) -> None:
m_load = mocker.patch("betty.load.load", new_callable=AsyncMock)

await new_temporary_app.project.configuration.write()
_run(
run(
"-c",
str(new_temporary_app.project.configuration.configuration_file_path),
"generate",
Expand Down Expand Up @@ -246,7 +246,7 @@ async def test(self, mocker: MockerFixture, new_temporary_app: App) -> None:
)
await new_temporary_app.project.configuration.write()
await makedirs(new_temporary_app.project.configuration.www_directory_path)
_run(
run(
"-c",
str(new_temporary_app.project.configuration.configuration_file_path),
"serve",
Expand Down
6 changes: 5 additions & 1 deletion betty/tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from betty.functools import Do
from betty.locale import DEFAULT_LOCALIZER
from betty.project import ProjectConfiguration
from betty.serde.dump import DictDump, Dump
from betty.serde.format import Format, Json, Yaml
from betty.subprocess import run_process

Expand All @@ -37,8 +38,11 @@ class TestDocumentation:
async def test_should_contain_cli_help(self) -> None:
async with TemporaryDirectory() as working_directory_path_str:
working_directory_path = Path(working_directory_path_str)
configuration = {
configuration: DictDump[Dump] = {
"base_url": "https://example.com",
"extensions": {
"betty.extension.Nginx": {},
},
}
async with aiofiles.open(working_directory_path / "betty.json", "w") as f:
await f.write(json.dumps(configuration))
Expand Down
1 change: 1 addition & 0 deletions documentation/usage/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ After installing Betty :doc:`via pip </installation/pip>` or :doc:`from source <
update-translations Update all existing translations
generate Generate a static site.
serve Serve a generated site.
serve-nginx-docker Serve a generated site with nginx in a Docker...
Generally you will be using Betty for a specific site. When you call ``betty`` with a
:doc:`configuration file <usage/project/configuration>` (e.g. ``betty -c betty.yaml``), additional commands provided by the extensions
Expand Down

0 comments on commit b99abf6

Please sign in to comment.