Skip to content

Commit

Permalink
feat: Allow changing working directory for code blocks execution
Browse files Browse the repository at this point in the history
Issue-20: #20
  • Loading branch information
pawamoy committed Jun 13, 2024
1 parent 3c5ca75 commit ee3fae9
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 2 deletions.
15 changes: 15 additions & 0 deletions docs/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ linking to their related documentation:
- [`session`](#sessions): Execute code blocks within a named session, reusing previously defined variables, etc..
- [`source`](#render-the-source-code-as-well): Render the source as well as the output.
- [`tabs`](#change-the-titles-of-tabs): When rendering the source using tabs, choose the tabs titles.
- [`workdir`](#change-the-working-directory): Change the working directory.
- [`title`](#additional-options): Title is a [Material for MkDocs][material] option.
- [`updatetoc`](#generated-headings-in-table-of-contents): Whether to update the Table of Contents with generated headings.

Expand Down Expand Up @@ -264,6 +265,20 @@ $ cat .git/config
WARNING: **Limitation**
Wrapping the result is not possible when HTML output is enabled.

## Change the working directory

To change the working directory for the execution of a code block, use the `workdir` option.

````md exec="1" source="tabbed-left"
```bash exec="1"
pwd
```

```bash exec="1" workdir=".."
pwd
```
````

## Additional options

If you are using [Material for MkDocs][material],
Expand Down
2 changes: 2 additions & 0 deletions src/markdown_exec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def validator(
update_toc_value = _to_bool(inputs.pop("updatetoc", "yes"))
tabs_value = inputs.pop("tabs", "|".join(default_tabs))
tabs = tuple(_tabs_re.split(tabs_value, maxsplit=1))
workdir_value = inputs.pop("workdir", None)
options["id"] = id_value
options["id_prefix"] = id_prefix_value
options["html"] = html_value
Expand All @@ -85,6 +86,7 @@ def validator(
options["session"] = session_value
options["update_toc"] = update_toc_value
options["tabs"] = tabs
options["workdir"] = workdir_value
options["extra"] = inputs
return True

Expand Down
27 changes: 25 additions & 2 deletions src/markdown_exec/formatters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

import os
from contextlib import contextmanager
from textwrap import indent
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any, Callable, Iterator
from uuid import uuid4

from markupsafe import Markup
Expand All @@ -18,6 +20,24 @@
default_tabs = ("Source", "Result")


@contextmanager
def working_directory(path: str | None = None) -> Iterator[None]:
"""Change the working directory for the duration of the context.
Parameters:
path: The path to change the working directory to.
"""
if path:
old_cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_cwd)
else:
yield


class ExecutionError(Exception):
"""Exception raised for errors during execution of a code block.
Expand Down Expand Up @@ -55,6 +75,7 @@ def base_format(
transform_source: Callable[[str], tuple[str, str]] | None = None,
session: str | None = None,
update_toc: bool = True,
workdir: str | None = None,
**options: Any,
) -> Markup:
"""Execute code and return HTML.
Expand All @@ -77,6 +98,7 @@ def base_format(
session: A session name, to persist state between executed code blocks.
update_toc: Whether to include generated headings
into the Markdown table of contents (toc extension).
workdir: The working directory to use for the execution.
**options: Additional options passed from the formatter.
Returns:
Expand All @@ -92,7 +114,8 @@ def base_format(
source_output = code

try:
output = run(source_input, returncode=returncode, session=session, id=id, **extra)
with working_directory(workdir):
output = run(source_input, returncode=returncode, session=session, id=id, **extra)
except ExecutionError as error:
identifier = id or extra.get("title", "")
identifier = identifier and f"'{identifier}' "
Expand Down
18 changes: 18 additions & 0 deletions tests/test_base_formatter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for the base formatter."""

import subprocess

import pytest
from markdown import Markdown

Expand Down Expand Up @@ -73,3 +75,19 @@ def test_dont_render_anything_if_output_is_empty(md: Markdown) -> None:
md=md,
)
assert not markup


def test_changing_working_directory(md: Markdown) -> None:
"""Assert we can change the working directory with `workdir`.
Parameters:
md: A Markdown instance (fixture).
"""
markup = base_format(
language="python",
run=lambda code, **_: subprocess.check_output(code, shell=True, text=True), # noqa: S602
code="pwd",
md=md,
workdir="/",
)
assert markup == "<p>/</p>"

0 comments on commit ee3fae9

Please # to comment.