Skip to main content

Create and run markdown Readmes from within pytest

Project description


author: gk version: 190918


Generating Markdown - While Testing Contained Claims

Build Status codecovPyPI    version

Table Of Contents

Few things are more annoying than stuff which does not work as announced, especially when you find out only after an invest of time and energy.

Documentation is often prone to produce such situations, since hard to keep 100% in sync with the code evolution.

This is a set of tools, generating documentation, while verifying the documented claims about code behaviour - without the need to adapt the source code, e.g. by modifying doc strings:

When the documentation is using a lot of code examples then a very welcome additional benefit of writing it like shown is the availability of source code autoformatters.

Other Example:

This "README.md" was built into this template, where html comment style placeholders had been replaced while running pytest on test_tutorial.

Lets run a bash command and assert on its results. Note that the command is shown in the readme, incl. result and the result can be asserted upon.

$ cat "/etc/hosts" | grep localhost
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
$ ls "/home/gk/repos/pytest2md/tests"
assets
__pycache__
test_basics.py
test_changelog.py
test_tutorial.py

$ ls -lta /etc/hosts
-rw-r--r--. 1 root root 286 Jul 22 12:08 /etc/hosts

Generated by:

Inline Python Function Execution

via the md_from_source_code function you can write fluent markdown (tested) python combos:

hi = 'hello world'
assert 'world' in hi
print(hi.replace('world', 'joe'))

The functions are evaluated and documented in the order they show up within the textblocks.

Please keep tripple apostrophes - we split the text blocks, searching for those.

State is kept within the outer pytest function, like normally in python. I.e. if you require new state, then start a new pytest function.

Stdout is redirected to an output collector function, i.e. if you print this does result in an "Output" block. If the printout starts with "MARKDOWN:" then we won't wrap that output into fenced code blocks but display as is.

If the string 'breakpoint' occurs in a function body, we won't redirect standardout for displaying output.

Features

html_table

ht = p2m.html_table

print(ht([['foo', 'bar'], ['bar', 'baz']], ['h1', 'h2']))
print('As details when summary arg is given:')
t = ht(
    [['joe', 'doe'], ['suzie', 'wong']],
    ['first', 'last'],
    summary='names. click to open...',
)
assert 'details' in t
assert 'joe</td' in t
print(t)

sh_file

$ cat "test_file.json"
{
    "a": [
        {
            "testfile": "created"
        },
        "at",
        "Wed Sep 25 12:48:46 2019"
    ]
}
as details
$ cat "test_file.json"
{
    "a": [
        {
            "testfile": "created"
        },
        "at",
        "Wed Sep 25 12:48:46 2019"
    ]
}

test_file.json

as link


Generated by:

# if content is given it will create it:
p2m.sh_file(fn, lang='javascript', content=c)
# summary arg creates a details structure:
p2m.sh_file(fn, lang='javascript', content=c, summary='as details')
# creates a link (say True to have the filename as link text)
p2m.sh_file(fn, lang='javascript', content=c, as_link=True)
p2m.sh_file(fn, lang='javascript', content=c, as_link='as link')

bash_run

$ ./some_non_existing_command_in_assets arg1
/bin/sh: /home/gk/repos/pytest2md/tests/assets/some_non_existing_command_in_assets: No such file or directory
$ ls -lta | grep total | head -n 1
total 80
$ ls -lta
total 80
-rw-r--r--.  1 gk armynyus  2025 Sep 25 12:48 .README.tmpl.md
drwxr-xr-x.  8 gk armynyus  4096 Sep 25 12:48 .git
drwxrwxr-x. 21 gk gk        4096 Sep 23 17:41 ..
drwxr-xr-x.  8 gk armynyus  4096 Sep 22 18:32 .

...(output truncated - see link below)
$ ls -lta

ls -lta


Generated by:

run = partial(p2m.bash_run, cmd_path_from_env=True)
# by default we search in normal environ for the command to run
# but we provide a switch to search in test assets.
# errors are redirected to stdout
res = p2m.bash_run(
    'some_non_existing_command_in_assets arg1',
    cmd_path_from_env=False,
    ign_err=True,
)
assert res[0]['exitcode'] != 0
res = run('ls -lta | grep total | head -n 1')
assert 'total' in res[0]['res']
res = run('ls -lta', into_file='bash_run.txt')
assert 'total' in res[0]['res']
# Ending with .html it converts ansi escape colors to html:
# simple link is created.
# (requires pip install ansi2html)
run('ls -lta', into_file='bash_run.html')

md_from_source_code

Inserted markdown from running python.

Strings in double apos. are rendered, no need to call a render function.

md('Inserted markdown from running python.')
print('From output of running python ')

Output:

From output of running python

More markdown

print('From another function')

Output:

From another function

Strings can also contain instructions, like this (looked up in p2m.MdInline namespace class)

$ cd /etc; ls -lta | head -n 5
total 2240
drwxr-xr-x. 156 root root     12288 Sep 23 00:55 .
-rw-r--r--.   1 root root        67 Sep 23 00:55 resolv.conf
drwxr-xr-x.   5 root root      4096 Sep 19 19:23 systemd
-rw-r--r--.   1 root root    124464 Sep 19 19:00 ld.so.cache

Default inline functions (add your own in module headers):

print(
    [
        k
        for k in dir(pytest2md.MdInline)
        if not k.startswith('_')
    ]
)

Output:

['bash', 'sh_file']

Generated by

        def some_test_function():
            """
            Strings in double apos. are rendered, no need to call a render function.
            """

            def func1():
                md('Inserted markdown from running python.')
                print('From output of running python ')

            """
            > More  markdown
            """

            def func2():
                print('From another function')

            """
            Strings can also contain instructions, like this (looked up in p2m.MdInline namespace class)

            <bash: cd MY_REPL_DIR; ls -lta | head -n 5>

            Default inline functions (add your own in module headers):
            """

            def known():
                print(
                    [
                        k
                        for k in dir(pytest2md.MdInline)
                        if not k.startswith('_')
                    ]
                )

            # repl dict simply replaces keys with values before any processing:
            p2m.md_from_source_code(repl={'MY_REPL_DIR': '/etc'})

Table of Contents

p2m.write_markdown(with_source_ref=True, make_toc=True)

See this tutorial.

Link Replacements

Technical markdown content wants to link to source code often. How to get those links working and that convenient?

The module does offer also some source finding / link replacement feature, via the mdtool module. The latter link was built simply by this:

[mdtool]<SRC>

Other example: This test_tutorial.py link was created by replacing "SRC" with the path to a file matching, under a given directory, prefixed by an arbitrary base URL.

Spec

These will be replaced:

[title:this,fmatch:test_tutorial,lmatch:line_match] <SRC> (remove space between] and <)

  • title: The link title - text the user reads
  • fmatch: substring match for the link destination file
  • lmatch: Find matching line within that file
  • show_raw: Link to raw version of file, not the one rendered by the repo server
  • path: Fix file path (usually derived by fmach)
  • line: Fix the line number of the link (usually done via lmatch)

Code Repo Hoster Specific Source Links

Github, Gitlab, Bitbucked or Plain directory based static content servers all have their conventional URLs regarding those links.

Since all of these are just serving static content w/o js possibilities, you have to parametrize the intended hoster in your environment, before running a pytest / push cycle. That way the links will be working on the hoster.

Currently we understand the following namespaces for links:

{
    "github": "https://github.com/%(gh_repo_name)s/blob/%(git_rev)s/%(path)s%(line:#L%s)s",
    "github_raw": "https://raw.githubusercontent.com/%(gh_repo_name)s/%(git_rev)s/%(path)s%(line:#L%s)s",
    "static": "file://%(d_repo_base)s/%(path)s",
    "static_raw": "file://%(d_repo_base)s/%(path)s"
}

Setting a link template

  • export MD_LINKS_FOR=github # before running pytest / push
  • <!-- md_links_for: github --> # in the markdown template, static

The latter can be overwritten by environ, should you want to push from time to time to a different code hoster.

Link Refs

We minimize the problem of varying generated target markdown, dependent on the hoster. How? Like any problem in IT is solved.

By building reference links the differences of e.g. a README.md for github vs. gitlab is restricted to the links section on the end of the generated markdown. In the markdown bodies you'll just see link names, which remain the same.

Check the end of the rendering result at the end of this README.md, in order to see the results for the hoster you are reading this markdown file currently.

Summary

  • At normal runs of pytest, the link base URL is just a local file:// link,

  • Before pushes one can set via environ (e.g. export MD_LINKS_FOR=github) these e.g. to the github base URL or the repo.

  • [key-values] constructs are supported as well, extending to beyond just the base url. Example following:

Source code showing is done like this:

    def test_sh_code(self):
        md('Source code showing is done like this:')
        p2m.sh_code(self.test_sh_code)
        md(
            '> Is [title:this,fmatch:test_tutorial,lmatch:exotic]<SRC> an exotic form of a recursion? ;-)  '
        )

Is this an exotic form of a recursion? ;-)

Command Summary
cat "/etc/hosts" | grep localhost
ls "/home/gk/repos/pytest2md/tests"
ls -lta /etc/hosts
/home/gk/repos/pytest2md/tests/assets/some_non_existing_command_in_assets arg1
ls -lta | grep total | head -n 1
ls -lta
ls -lta

*Auto generated by pytest2md, running ./tests/test_tutorial.py

Tips

  • Local Renderer:

    pip install grip

to get a local github compliant markdown renderer, reloading after changes of the generated markdown.

  • Using fixtures with unittest style test classes

Create a conftest.py like:

root@localhost tests]# cat conftest.py
import pytest
import pytest2md as p2m


@pytest.fixture(scope='class')
def write_md(request):
    def fin():
        request.cls.p2m.write_markdown(with_source_ref=True, make_toc=False)

    request.addfinalizer(fin)

then use like:

@pytest.mark.usefixtures('write_md')
class TestDevUsage(unittest.TestCase):
    p2m = p2m # on module level: p2m.P2M(__file__, fn_target_md='README.md')

    (...)

Isolation

None. If you would screw up your host running pytest normally, then you will get the same result, when running markdown generating tests.


Here is a bigger tutorial, from pytest2md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest2md-190918.tar.gz (40.5 kB view details)

Uploaded Source

Built Distribution

pytest2md-190918-py3-none-any.whl (35.1 kB view details)

Uploaded Python 3

File details

Details for the file pytest2md-190918.tar.gz.

File metadata

  • Download URL: pytest2md-190918.tar.gz
  • Upload date:
  • Size: 40.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.14.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.35.0 CPython/3.7.3

File hashes

Hashes for pytest2md-190918.tar.gz
Algorithm Hash digest
SHA256 940f9f86c8f45f27330927863b6527c50d4850ea117ecd3633b1c476f07f5fa6
MD5 9958ba028132bb150c4d99ad09e96628
BLAKE2b-256 d462739c2ddf7e13dc2cfa1cd07ff796d25c6a6d913a98d29d5b8cbea246ff06

See more details on using hashes here.

File details

Details for the file pytest2md-190918-py3-none-any.whl.

File metadata

  • Download URL: pytest2md-190918-py3-none-any.whl
  • Upload date:
  • Size: 35.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.14.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.35.0 CPython/3.7.3

File hashes

Hashes for pytest2md-190918-py3-none-any.whl
Algorithm Hash digest
SHA256 a83c7924c5cb3af77a99eb4408fd65836fcf2d88b39d48238ade42f2508d1e19
MD5 3924ad3d2ccc23e4240694913db0e617
BLAKE2b-256 c9bc4b7283f13606c6253903b0d3158a38213d266baa8f10a54063065dba4115

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page