docs: Pull tutorial code snippets from examples/ - #2544
Conversation
|
This is super cool! I didn't know Sphinx could do this. |
| ```{literalinclude} ../../../examples/k8s-1-minimal/charmcraft.yaml | ||
| :language: yaml | ||
| :start-at: 'title: Web Server Demo' | ||
| :end-at: how to write a Kubernetes charm with Ops. | ||
| ``` |
There was a problem hiding this comment.
We could use start-after and end-after with comment anchors in cases where we worry that this won't be robust. Thinking about this case specifically, I guess Sphinx would yell at us if we changed the end line of the description so there was no match, so this seems great as-is.
Per review feedback, exclude docs/tutorial/ from ruff entirely (the tutorial will be migrated to literalinclude separately in canonical#2544) and drop the second 'quote-style = preserve' ruff format pass. The remaining docs now use single quotes consistently with the rest of the project. Also add docs/ruff.toml with line-length = 80 so code snippets in the docs wrap at a width that doesn't require horizontal scrolling when rendered. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
d85517a to
ea9b041
Compare
Replace hardcoded code fences in the tutorial pages with Sphinx literalinclude directives that pull from the matching example charms in examples/. This keeps the tutorial automatically in sync with the canonical source, and removes ~750 lines of duplicated code. Strategy per snippet type: - Full method or class: :pyobject: Class.method - Single line or small fragment: :start-at:/:end-at: patterns on the line content (no marker comments needed in sources) - Multi-line block: :start-at:/:end-before: patterns anchored on the next section - Full file (skipping copyright header): :start-at: with the first real line Intermediate states that don't exist in examples/ (e.g. the empty starting charm class, lines shown only to be removed, contrastive "don't do this" snippets, and a troubleshooting timeout hint) are left inline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ea9b041 to
5a173ed
Compare
james-garner-canonical
left a comment
There was a problem hiding this comment.
I haven't checked every example, but I really like this direction.
I assume that if a matching anchor isn't found, the docs build complains loudly, so this should be far less prone to drift that before.
I have one non-blocking concern that may be worth addressing at this point: the maximum line length we use in the charm code can lead to long lines that require horizontal scrolling in the docs -- previously we would have reformatted them manually in the docs to avoid this. Should we use a shorter maximum line length for the examples now that we're using them this way? Does this have implication for the profiles -- I think it would be fine if this was one of our divergences from the profiles, but maybe it would make the tutorial harder to follow, in which case maybe we just live with the horizontal scrolling ...
| :start-at: framework.observe(self.on["demo-server"] | ||
| :end-at: framework.observe(self.on["demo-server"] |
There was a problem hiding this comment.
This line is too long and requires scrolling in the docs. I wonder if we should adopt a lower line length for the example charms now that we're using them this way.
There was a problem hiding this comment.
Thank you for implementing this.
I checked that the Sphinx build errors if a literalinclude doesn't match the charm source. That means that even the almost trivial includes (e.g., the one-liners) have value, as we're kind of asserting the charm source hasn't changed since the tutorial was last updated.
There is one exception where I feel literalinclude is making the doc look worse, and where I think it's worth losing the assertion. Noted in a suggestion.
Re line lengths: I would prefer if the docs had enough space to comfortably show all lines. Next cycle we need to switch to the Ulwazi theme, which should help a bit, but not that much.
I'm not keen on setting a lower line width in the example charms - deviating from the Charmcraft profiles. I'm not keen on modifying the profiles either. For now, I'm happy to accept that some lines will scroll.
Actually, related to this, I noticed that some literalincludes set :dedent: and some don't. I think this matches preexisting differences between the machine and K8s tutorials. My observation has been that people like being able to copy/paste from the tutorial snippets. Does dedent make that harder?
I think we should make a decision on dedent (for the tutorials) as part of this PR. As I write this, I lean towards removing all dedents from the tutorial snippets. This would make the line-length problem worse, though.
| :start-at: '@pytest.fixture(scope="module")' | ||
| :end-at: yield juju_factory.get_juju |
There was a problem hiding this comment.
This will work even though the function has a decorator. I verified by building locally.
| :start-at: '@pytest.fixture(scope="module")' | |
| :end-at: yield juju_factory.get_juju | |
| :pyobject: cos |
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | ||
| :language: python | ||
| :start-at: def _on_stop | ||
| :end-at: tinyproxy.uninstall() | ||
| ``` |
There was a problem hiding this comment.
I think we should switch to pyobject for both methods, matching what we do for "Deploy COS Lite and integrate with Loki" in the machine charm tutorial.
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | |
| :language: python | |
| :start-at: def _on_stop | |
| :end-at: tinyproxy.uninstall() | |
| ``` | |
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | |
| :language: python | |
| :pyobject: TinyproxyCharm._on_stop | |
| ``` | |
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | |
| :language: python | |
| :pyobject: TinyproxyCharm._on_remove | |
| ``` |
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | ||
| :language: python | ||
| :start-at: import time | ||
| :end-at: import time | ||
| ``` | ||
|
|
||
| PORT = 8000 | ||
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | ||
| :language: python | ||
| :start-at: PORT = 8000 | ||
| :end-at: PORT = 8000 |
There was a problem hiding this comment.
Having two code blocks looks strange in the rendered doc. I think this would be better as:
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | |
| :language: python | |
| :start-at: import time | |
| :end-at: import time | |
| ``` | |
| PORT = 8000 | |
| ```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py | |
| :language: python | |
| :start-at: PORT = 8000 | |
| :end-at: PORT = 8000 | |
| ```python | |
| import time | |
| PORT = 8000 |
And we accept that we lose validation that the example charm also contains these lines. I think in this case (only) it's an acceptable trade-off.
There was a problem hiding this comment.
What if we put text between the blocks? "Add the following import to src/charm.py:", "Define the PORT constant like this:".
Unrelated, I was briefly confused that these weren't referred to anywhere below, until I realised that they were the requirements for the code block above them to run. I assume this was an intentional choice, but if reordering them would make the separate blocks flow better, that could be an option too.
|
Re: to def foo():
x = []
x.append(1)
# We want to insert some text below this line.
# But above this line.
return xAnd we want to insert the lines: x.append(2)
x.append(3)I'm not sure what the UX would be that would reliably let you copy exactly the lines snippet, and paste it into the existing file, because you'll first need to insert a linebreak after def foo():
x = []
x.append(1)
# We want to insert some text below this line.
x.append(2)
x.append(3)
# But above this line.
return xOn the other hand if the lines were dedented, you'd get this: def foo():
x = []
x.append(1)
# We want to insert some text below this line.
x.append(2)
x.append(3)
# But above this line.
return xI guess indented snippets could start with a linebreak, but would users actually realise to paste at the end of the preceding line? So maybe we should I don't mind if the rule we go with is "never |
Replace hardcoded code fences in the tutorial pages with Sphinx
literalincludedirectives that pull from the matching example charms inexamples/. This keeps the tutorial automatically in sync with the canonical source, and removes ~750 lines of duplicated code.Strategy per snippet type:
:pyobject: Class.method:start-at:/:end-at:patterns on the line content (no marker comments needed in sources):start-at:/:end-before:patterns anchored on the next section:start-at:with the first real lineIntermediate states that don't exist in
examples/(for example, the empty starting charm class, lines shown only to be removed, contrastive "don't do this" snippets, and a troubleshooting timeout hint) are left inline.Preview