Skip to content

docs: Pull tutorial code snippets from examples/ - #2544

Open
tonyandrewmeyer wants to merge 1 commit into
canonical:mainfrom
tonyandrewmeyer:tutorial-pull-from-files
Open

docs: Pull tutorial code snippets from examples/#2544
tonyandrewmeyer wants to merge 1 commit into
canonical:mainfrom
tonyandrewmeyer:tutorial-pull-from-files

Conversation

@tonyandrewmeyer

@tonyandrewmeyer tonyandrewmeyer commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

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/ (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

@james-garner-canonical

Copy link
Copy Markdown
Contributor

This is super cool! I didn't know Sphinx could do this.

Comment on lines +57 to 61
```{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.
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

tonyandrewmeyer added a commit to tonyandrewmeyer/operator that referenced this pull request Jun 8, 2026
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>
@tonyandrewmeyer
tonyandrewmeyer force-pushed the tutorial-pull-from-files branch from d85517a to ea9b041 Compare September 2, 2026 01:12
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>
@tonyandrewmeyer
tonyandrewmeyer force-pushed the tutorial-pull-from-files branch from ea9b041 to 5a173ed Compare September 2, 2026 01:16
@tonyandrewmeyer
tonyandrewmeyer marked this pull request as ready for review September 2, 2026 01:16

@james-garner-canonical james-garner-canonical left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ...

Comment on lines +131 to +132
:start-at: framework.observe(self.on["demo-server"]
:end-at: framework.observe(self.on["demo-server"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dwilding dwilding left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +479 to +480
:start-at: '@pytest.fixture(scope="module")'
:end-at: yield juju_factory.get_juju

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will work even though the function has a decorator. I verified by building locally.

Suggested change
:start-at: '@pytest.fixture(scope="module")'
:end-at: yield juju_factory.get_juju
:pyobject: cos

Comment on lines +383 to +387
```{literalinclude} ../../examples/machine-tinyproxy/src/charm.py
:language: python
:start-at: def _on_stop
:end-at: tinyproxy.uninstall()
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
```{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
```

Comment on lines +308 to +317
```{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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having two code blocks looks strange in the rendered doc. I think this would be better as:

Suggested change
```{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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@james-garner-canonical

james-garner-canonical commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Re: to :dedent: or not to :dedent:, I suspect there's not a perfect solution for fragments, because of how editors will automatically indent the first line anyway. For example, say the reader has this in their file already:

def foo():
    x = []
    x.append(1)
    # We want to insert some text below this line.
    # But above this line.
    return x

And 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 below this line., and your editor will almost definitely indent the new line. So if you paste the lines above, you'll 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 x

On 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 x

I 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 :dedent: everything? Or maybe there are some cases where it's useful and some where it's not?

I don't mind if the rule we go with is "never :dedent:", this just got me thinking and I thought I'd share in case you agree or in case your experience is different (and so I'm missing something).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants