My csvs-to-sqlite README includes a section that shows the output of the csvs-to-sqlite --help
command (relevant issue).
I had been manually copying this in, but I decided to try using cog to automate the process.
Here's what I came up with:
<!-- [[[cog
import cog
from csvs_to_sqlite import cli
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(cli.cli, ["--help"])
help = result.output.replace("Usage: cli", "Usage: csvs-to-sqlite")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
Usage: csvs-to-sqlite [OPTIONS] PATHS... DBNAME
...
```
<!-- [[[end]]] -->
Then to update the README file, run this:
cog -r README.md
The -r
option causes it to modify that file in place.
Cog works by scanning for a [[[cog ... ]]]
section, executing the code there, capturing the cog.out()
output and using that to replace everything from the end of the code block up to the line containing the [[[end]]]
marker.
It's designed to interact well with comments - in this case HTML comments - such that the cog
generation code can be hidden.
A version of Cog released after I first wrote this TIL added a new --check
option, so you can run a test in CI to check if the file needs to be updated using:
cog --check README.md
Any time I generate content like this in a repo I like to include a test that will fail if I forget to update the content.
cog
clearly isn't designed to be used as an independent library, but I came up with the following pattern pytest
test which works well, in my tests/test_csvs_to_sqlite.py
module:
from cogapp import Cog
import sys
from io import StringIO
import pathlib
def test_if_cog_needs_to_be_run():
_stdout = sys.stdout
sys.stdout = StringIO()
readme = pathlib.Path(__file__).parent.parent / "README.md"
result = Cog().main(["cog", str(readme)])
output = sys.stdout.getvalue()
sys.stdout = _stdout
assert (
output == readme.read_text()
), "Run 'cog -r README.md' to update help in README"
The key line here is this one:
result = Cog().main(["cog", str(readme)])
In cog's implementation, that code is called like this:
Cog().main(sys.argv)
Here I'm faking the command-line arguments to pass in just the path to my README.md
file.
Cog then writes the generated output to stdout
- which I capture with that sys.stdout
trick.
Finally, I compare the generated output to the current file content and fail the test with a reminder to run cog -r
if they do not match.
Here's an example of cog
in a .rst
file:
.. [[[cog
import tabulate
cog.out("\n" + "\n".join('- ``{}``'.format(t) for t in tabulate.tabulate_formats) + "\n\n")
.. ]]]
- ``fancy_grid``
- ``fancy_outline``
- ``github``
.. [[[end]]]
The trailing and leading newlines are important to avoid a warning about "Explicit markup ends without a blank line; unexpected unindent".
I added these to both Datasette and sqlite-utils
: full pages that list the help for every command provided by those tools.
Created 2021-11-18T08:27:19-08:00, updated 2022-02-12T08:07:17-08:00 · History · Edit