Skip to content

Commit e99300f

Browse files
committed
Add ability to read from file and find start point
This is mainly for reading gallery examples into the user docs and avoid code duplication. - altair-plot now stores code_source_file metadata on the generated altair_plot node when a file argument is used. - Kept inline behavior intact. - Added safer file-arg detection so normal inline code isn’t misinterpreted as a path.
1 parent 777d56e commit e99300f

File tree

5 files changed

+67
-1
lines changed

5 files changed

+67
-1
lines changed

sphinxext_altair/altairplot.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from docutils import nodes
7070
from docutils.parsers.rst import Directive
7171
from docutils.parsers.rst.directives import flag, unchanged
72+
from sphinx.directives.code import LiteralIncludeReader
7273
from sphinx.locale import _
7374

7475
import altair as alt
@@ -156,6 +157,9 @@ def validate_output(output: str) -> str:
156157

157158
class AltairPlotDirective(Directive):
158159
has_content: ClassVar[bool] = True
160+
required_arguments: ClassVar[int] = 0
161+
optional_arguments: ClassVar[int] = 1
162+
final_argument_whitespace: ClassVar[bool] = True
159163
option_spec: ClassVar[dict[str, Callable[[str], Any]]] = {
160164
"hide-code": flag,
161165
"remove-code": flag,
@@ -167,8 +171,45 @@ class AltairPlotDirective(Directive):
167171
"chart-var-name": unchanged,
168172
"strict": flag,
169173
"div_class": strip_lower,
174+
"start-after": unchanged,
170175
}
171176

177+
_code_source_file: str | None = None
178+
179+
def _read_code(self, env: BuildEnvironment) -> str:
180+
self._code_source_file = None
181+
has_file_arg = False
182+
if len(self.arguments) == 1 and not self.content:
183+
candidate = self.arguments[0]
184+
if "\n" not in candidate:
185+
_, filename = env.relfn2path(candidate)
186+
if Path(filename).is_file():
187+
has_file_arg = True
188+
if has_file_arg:
189+
rel_filename, filename = env.relfn2path(self.arguments[0])
190+
env.note_dependency(rel_filename)
191+
self._code_source_file = rel_filename
192+
include_options: dict[str, Any] = {}
193+
if "start-after" in self.options:
194+
include_options["start-after"] = self.options["start-after"]
195+
code, _ = LiteralIncludeReader(
196+
filename, include_options, env.app.config
197+
).read(location=(self.state_machine.document["source"], self.lineno))
198+
return code
199+
200+
if "start-after" in self.options and not has_file_arg:
201+
msg = ":start-after: can only be used with a file path argument"
202+
raise ValueError(msg)
203+
204+
if self.arguments:
205+
# Keep backwards compatibility when no blank line appears after
206+
# ``.. altair-plot::`` by treating parsed arguments as code text.
207+
prefix = " ".join(self.arguments)
208+
lines = [prefix, *self.content]
209+
return "\n".join(lines)
210+
211+
return "\n".join(self.content)
212+
172213
def run(self) -> list[nodes.Element]:
173214
env = t.cast("BuildEnvironment", self.state.document.settings.env)
174215
app = env.app
@@ -183,7 +224,7 @@ def run(self) -> list[nodes.Element]:
183224
)
184225

185226
# Show code
186-
code = "\n".join(self.content)
227+
code = self._read_code(env)
187228
source_literal = nodes.literal_block(code, code)
188229
source_literal["language"] = "python"
189230

@@ -209,6 +250,7 @@ def run(self) -> list[nodes.Element]:
209250
relpath=os.path.relpath(rst_fp.parent, env.srcdir),
210251
rst_source=rst_source,
211252
rst_lineno=self.lineno,
253+
code_source_file=self._code_source_file,
212254
links=self.options.get("links", app.builder.config.altairplot_links),
213255
output=self.options.get("output", "plot"),
214256
strict="strict" in self.options,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
project = "test-altairplot-filearg"
2+
extensions = ["sphinxext_altair.altairplot"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
print("before-marker")
2+
# START-AFTER-MARKER
3+
print("after-marker")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
File argument test
2+
==================
3+
4+
.. altair-plot:: from_file_source.py
5+
:output: stdout
6+
7+
8+
.. altair-plot:: from_file_source.py
9+
:output: stdout
10+
:start-after: # START-AFTER-MARKER

tests/test_altairplot.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,12 @@ def test_altairplotdirective(app: Sphinx) -> None:
252252
</script>
253253
</div>"""
254254
assert block_plot_7 in result
255+
256+
257+
@pytest.mark.sphinx(testroot="altairplot-filearg", freshenv=True)
258+
def test_altairplotdirective_file_argument(app: Sphinx) -> None:
259+
app.builder.build_all()
260+
result = (app.outdir / "index.html").read_text(encoding="utf8")
261+
assert result.count("before-marker") == 2
262+
assert result.count("after-marker") == 4
263+
assert result.count("# START-AFTER-MARKER") == 1

0 commit comments

Comments
 (0)