phmutest


Namephmutest JSON
Version 1.0.0 PyPI version JSON
download
home_pagehttps://phmutest.readthedocs.io/en/latest/
SummaryDetect and troubleshoot broken Python examples in Markdown.
upload_time2025-02-08 21:50:43
maintainerNone
docs_urlNone
authorMark Taylor
requires_python>=3.8
licenseMIT
keywords documentation markdown testing
VCS
bugtrack_url
requirements tomli
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # phmutest 1.0.0

## Detect and troubleshoot broken Python examples in Markdown

- Hybrid Python library / console program checks Python syntax highlighted examples.
- Python tools to get fenced code block contents from Markdown. | [Here](docs/api.md)

Treats each Markdown file as a single long example, which continues
across multiple Markdown [fenced code blocks][3] (FCBs or blocks).

[Skip example and jump down to Features](#features)

## A broken Example

When tests fail we show what caused the error to help you quickly find the root cause.
This example shows how to use the example library answerlib
| [answerlib.py](docs/answerlib_py.md).
It answers a question put to the ask method. | [phmutest output](#phmutest-console-output)

```python
from docs.answerlib import RightAnswer, WrongAnswer, RaiserBot
```

Create a RightAnswer instance and ask a question.
The assert statement checks the answer.
phmutest assigns a pass/failed/error/skip status to each Python FCB.
This FCB is given 'pass' status.
Note how the example continues across multiple FCBs.
It continues for the entire Markdown file.

### pass result

```python
pass_bot = RightAnswer()
answer = pass_bot.ask(question="What floats?")
assert answer == "apples"
```

### failed result

Create a WrongAnswer instance and ask a question.
The WrongAnswer instance ask() method returns an
incorrect answer.
The assert statement checks the answer,
finds that
it is wrong and raises an AssertionError.
This FCB is given 'failed' status.

```python
fail_bot = WrongAnswer()
answer = fail_bot.ask(question="What floats?")
assert answer == "apples"
```

### error result

Now we are going to cause the answerlib to raise an
exception by calling the method inquire() which does not exist.
This raises an AttributeError in the library which propagates
up and out of the first line of the FCB below.
This FCB is given 'error' status.

```python
answer = pass_bot.inquire(query="What floats?")
assert answer == "apples"
```

The test runner keeps going even after an exception. To stop
on first failure use the "-f" option.

```python
answer = pass_bot.ask(question="What floats?")
assert answer == "apples"
```

Cause another exception within answerlib to see the FCB line
where the exception propagates out of the FCB in the log.
This FCB is also given 'error' status. See the results in the
log below.

```python
raiser_bot = RaiserBot()
_ = raiser_bot.ask(question="What floats?")
```

### Checking expected output

Add an FCB that immediately follows a Python code block that has no info string
or the info string `expected-output`. Captured stdout is compared to the block.
In the log a "o" after the filename indicates expected output was checked.

```python
print("Incorrect expected output.")
```

```expected-output
Hello World!
```

### phmutest command line

```shell
phmutest README.md --log --quiet
```

### phmutest console output

There are two parts:

- unittest printing to sys.stderr
- phmutest printing to sys.stdout

#### phmutest stdout

This shows the --log output.
Below the log table are the broken FCB Markdown source file lines.

- The location is the file and line number of the opening fence of the FCB.
- The ">" indicates the line that raised the exception.

```txt
log:
args.files: 'README.md'
args.log: 'True'

location|label  result  reason
--------------  ------  ---------------------------------------------------------------
README.md:20..  pass
README.md:33..  pass
README.md:49..  failed  AssertionError
README.md:63..  error   AttributeError: 'RightAnswer' object has no attribute 'inquire'
README.md:71..  pass
README.md:81..  error   ValueError: What was the question?
README.md:92 o  failed
--------------  ------  ---------------------------------------------------------------

README.md:49
    50  fail_bot = WrongAnswer()
    51  answer = fail_bot.ask(question="What floats?")
>   52  assert answer == "apples"
        AssertionError

README.md:63
>   64  answer = pass_bot.inquire(query="What floats?")
        AttributeError: 'RightAnswer' object has no attribute 'inquire'

README.md:81
    82  raiser_bot = RaiserBot()
>   83  _ = raiser_bot.ask(question="What floats?")
        ValueError: What was the question?

README.md:92
    93  print("Incorrect expected output.")
AssertionError: 'Hello World!\n' != 'Incorrect expected output.\n'
- Hello World!
+ Incorrect expected output.
```

On GitHub, to see Markdown line numbers, view this file and choose
Code button. (Code is between Preview and Blame).

##### traceback

When phmutest is installed with the `[traceback]` extra,
a [stackprinter][21] formatted
traceback prints after each broken FCB. [Here](docs/traceback.md)
is an example traceback.

#### unittest stderr

Here is the unittest output printed to sys.stderr.
It starts with captured stdout/stderr from the 'error' FCBs.
Markdown Python FCBs are copied to a temporary 'testfile' that is
run by the unittest test runner. The test runner prints to stderr before
the phmutest stdout printing. The test runner output provides tracebacks
for the assertions and exceptions.
The testfile line numbers will mostly be different than the Markdown
line numbers. Look for the Markdown line numbers in the log. (Python 3.11)

```txt
=== README.md:81 stdout ===
This is RaiserBot.ask() on stdout answering 'What floats?'.
=== end ===
=== README.md:81 stderr ===
This is RaiserBot.ask() on stderr: Uh oh!
=== end ===
======================================================================
ERROR: tests (_phm1.Test001.tests) [README.md:63]
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\XXX\AppData\Local\Temp\YYY\_phm1.py", line 42, in tests
    answer = pass_bot.inquire(query="What floats?")
             ^^^^^^^^^^^^^^^^
AttributeError: 'RightAnswer' object has no attribute 'inquire'

======================================================================
ERROR: tests (_phm1.Test001.tests) [README.md:81]
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\XXX\AppData\Local\Temp\YYY\_phm1.py", line 55, in tests
    _ = raiser_bot.ask(question="What floats?")
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\XXX\Documents\u0\docs\answerlib.py", line 32, in ask
    raise ValueError("What was the question?")
ValueError: What was the question?

======================================================================
FAIL: tests (_phm1.Test001.tests) [README.md:49]
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\XXX\AppData\Local\Temp\YYY\_phm1.py", line 37, in tests
    assert answer == "apples"
           ^^^^^^^^^^^^^^^^^^
AssertionError

======================================================================
FAIL: tests (_phm1.Test001.tests) [README.md:92]
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\XXX\AppData\Local\Temp\YYY\_phm1.py", line 66, in tests
    _phm_testcase.assertEqual(_phm_expected_str, _phm_printer.stdout())
AssertionError: 'Hello World!\n' != 'Incorrect expected output.\n'
- Hello World!
+ Incorrect expected output.


----------------------------------------------------------------------
Ran 1 test in 0.003s

FAILED (failures=2, errors=2)
```

### Features

- Checks either Python code examples **or** ">>>" REPL examples
  | [doctest][5].
- Reports pass/failed/error/skip status and line number for each block.
- Shows block source indicating the line where the exception propagated.
- Support for setup and cleanup. Acquire and release resources, change context,
  Pass objects as global variables to the examples. Cleans up even when fail-fast.
  [Suite initialization and cleanup](#suite-initialization-and-cleanup)
- Write a pytest testfile into an existing pytest test suite.
- Runs files in user specified order.
- TOML configuration available.
- An example can continue **across** files.
- Show stdout printed by examples. --stdout
- Colors pass/failed/error/skip status. --color.
- Check expected output of code examples. Markdown edits are required.
- Designated and stable **patch points** for Python standard library
  **unittest.mock.patch()** patches. | [Here](#patch-points)

### Advanced features

These features require adding tool specific HTML comment **directives**
to the Markdown. Because directives are HTML comments they are not visible in
rendered Markdown. View directives on GitHub
by pressing the `Code` button in the banner at the top of the file.
| [Advanced feature details](docs/advanced.md).

- Assign test group names to blocks. Command line options select or
  deselect test groups by name.
- Skip blocks or skip checking printed output.
- Label any fenced code block for later retrieval.
- Accepts [phmdoctest][17] directives except share-names and clear-names.
- Specify blocks as setup and teardown code for the file or setup across files.

## main branch status

[![license](https://img.shields.io/pypi/l/phmutest.svg)](https://github.com/tmarktaylor/phmutest/blob/main/LICENSE)
[![pypi](https://img.shields.io/pypi/v/phmutest.svg)](https://pypi.python.org/pypi/phmutest)
[![python](https://img.shields.io/pypi/pyversions/phmutest.svg)](https://pypi.python.org/pypi/phmutest)

[![CI](https://github.com/tmarktaylor/phmutest/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tmarktaylor/phmutest/actions/workflows/ci.yml)
[![Build status](https://ci.appveyor.com/api/projects/status/nbu1xlraoii8x377?svg=true)](https://ci.appveyor.com/project/tmarktaylor/phmutest)
[![readthedocs](https://readthedocs.org/projects/phmutest/badge/?version=latest)](https://phmutest.readthedocs.io/en/latest/?badge=latest)
[![codecov](https://codecov.io/gh/tmarktaylor/phmutest/coverage.svg?branch=main)](https://codecov.io/gh/tmarktaylor/phmutest?branch=main)

[Docs RTD](https://phmutest.readthedocs.io/en/latest/) |
[Docs GitHub](https://github.com/tmarktaylor/phmutest/blob/main/README.md) |
[Repos](https://github.com/tmarktaylor/phmutest) |
[pytest][13] |
[Codecov](https://codecov.io/gh/tmarktaylor/phmutest?branch=main) |
[License](https://github.com/tmarktaylor/phmutest/blob/main/LICENSE)

[Installation](#installation) |
[Usage](#usage) |
[FILE](#file) |
[REPL mode](#repl-mode) |
[Suite initialization and cleanup](#suite-initialization-and-cleanup) |
[--color](#color-option) |
[--style](#style-option) |
[Extend an example across files](#extend-an-example-across-files) |
[Skip blocks from the command line](#skip-blocks-from-the-command-line) |
[--summary](#summary-option) |
[TOML configuration](#toml-configuration) |
[Run as a Python module](#run-as-a-python-module) |
[Call from Python](#call-from-python) |
[Patch points](#patch-points) |
[Hints](#hints) |
[Related projects](#related-projects) |
[Differences between phmutest and phmdoctest](#differences-between-phmutest-and-phmdoctest)

[Sections](docs/demos.md#sections) |
[Demos](docs/demos.md#demos) |
[Changelog](CHANGELOG.md) |
[Contributions](CONTRIBUTING.md)

See [list of demos](docs/demos.md)
See [How it works](docs/howitworks.md)

## Installation

```shell
python -m pip install phmutest
```

- No required dependencies since Python 3.11. Depends on tomli before Python 3.11.
- Pure Python. No binaries.
- It is advisable to install in a virtual environment.

### install extras

The extra 'color' enables the --color and
--style options.

```shell
python -m pip install "phmutest[color]"  # Windows
python -m pip install 'phmutest[color]'  # Unix/macOS
```

The extra 'pytest' installs pytest and the plugin
pytest-subtests.
pytest-subtests continues running subtests after
the first subtest failure. [pytest][20] prints a very
helpful traceback when FCBs break.

```shell
python -m pip install "phmutest[pytest]"  # Windows
python -m pip install 'phmutest[pytest]'  # Unix/macOS
```

The extra 'traceback' enables [stackprinter][21] traceback
printing for each broken FCB. The traceback is
slightly different than pytest's.

```shell
python -m pip install "phmutest[traceback]"  # Windows
python -m pip install 'phmutest[traceback]'  # Unix/macOS
```

Install with the extra 'dev' to install locally the same tools used by
the continuous integration scripts.

```shell
python -m pip install "phmutest[dev]"  # Windows
python -m pip install 'phmutest[dev]'  # Unix/macOS
```

Install with all the extras.

```shell
python -m pip install "phmutest[color, traceback, dev]"  # Windows
python -m pip install 'phmutest[color, traceback, dev]'  # Unix/macOS
```

## Usage

`phmutest --help`

```txt
usage: phmutest [-h] [--version] [--skip [TEXT ...]] [--fixture DOTTED_PATH.FUNCTION]
                [--share-across-files [FILE ...]]
                [--setup-across-files [FILE ...]] [--select [GROUP ...] | --deselect
                [GROUP ...]] [--config TOMLFILE] [--replmode] [--color]
                [--style STYLE] [-g OUTFILE]
                [--progress] [--sharing [FILE ...]] [--log] [--summary] [--stdout] [--report]
                [FILE ...]

Detect and troubleshoot broken Python examples in Markdown. Accepts relevant unittest options.

positional arguments:
  FILE                  Markdown input file.

options:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  --skip [TEXT ...]     Any block that contains the substring TEXT is not tested.
  --fixture DOTTED_PATH.FUNCTION
                        Function run before testing.
  --share-across-files [FILE ...]
                        Shares names from Markdown file to later positional files.
  --setup-across-files [FILE ...]
                        Apply Markdown file setup blocks to files.
  --select [GROUP ...]  Select all blocks with phmutest-group GROUP directive for testing.
  --deselect [GROUP ...]
                        Exclude all blocks with phmutest-group GROUP directive from testing.
  --config TOMLFILE     .toml configuration file.
  --replmode            Test Python interactive sessions.
  --color, -c           Enable --log pass/failed/error/skip result colors.
  --style STYLE         Specify a Pygments style name as STYLE to enable syntax highlighting.
  -g OUTFILE, --generate OUTFILE
                        Write generated Python or docstring to output file or stdout.
  --progress            Print block by block test progress. File by file in --replmode.
  --sharing [FILE ...]  For these files print name sharing. . means all files.
  --log                 Print log items when done.
  --summary             Print test count and skipped tests.
  --stdout              Print output printed by blocks.
  --report              Print fenced code block configuration, deselected blocks.
```

- The **-f** option indicates fail fast.

## FILE

The Markdown files are processed in the same order they are present as positional
arguments on the command line.
Shell wildcards can be used. Be aware that the shell expansion and operating system
will determine the order.

## REPL mode

When --replmode is specified Python interactive sessions are tested and
Python code and expected output blocks are not tested. REPL mode tests are
implemented using [doctest][5].
The option --setup-across-files and the setup and teardown directives
have no effect in REPL mode.
--progress has file by file granularity.
See the [Broken REPL example](docs/repl/REPLexample.md).

## Suite initialization and cleanup

For background refer to definitions at the top of [unittest][18].
Use --fixture to specify a Python initialization function that runs before the tests.
It works with or without --replmode, but there are differences.
In both modes, the fixture function may create objects (globs) that are visible
as globals to the FCBs under test.

In the event of test errors orderly cleanup/release of resources is assured.
For Python code blocks the fixture may register cleanup functions by
calling **unittest.addModuleCleanup()**.
In REPL mode the fixture function optionally returns a cleanup function.

- The fixture can acquire and release resources or change context.
- The fixture can make entries to the log displayed by --log.
- The fixture can install patches to the code under test.

Specify the --fixture function as
a relative **dotted path** where `/` is replaced with `.`.
For example, the function **my_init()** in the file **tests/myfixture.py**
would be specified:

--fixture **tests.myfixture.my_init**

The function is passed keyword only arguments and **optionally**
returns a Fixture instance.
The keyword arguments and return type are described by
[src/phmutest/fixture.py](docs/fixture_py.md).
The fixture file should be in the project directory tree. Fixture demos:

- [fixture change workdir](docs/fix/code/chdir.md)
- [fixture set globals](docs/fix/code/globdemo.md)
- [fixture cleanup REPL Mode](docs/fix/repl/drink.md)

The test case test_doctest_optionflags_patch() shows an
example with a fixture that applies a patch to
doctest optionflags in --replmode.

### Calling phmutest from pytest

In some of the tests the --fixture function is in the same pytest file as the
phmutest library call.  This is not recommended because the Python file is
imported again by fixture_function_importer() to a new module object.
The Python file's module level code will
be run a second time. If there are side-effects they will be repeated, likely
with un-desirable and hard to troubleshoot behavior.

### Dotted path details

The fixture function must be at the top level of a .py file.

- The dotted_path has components separated by ".".
- The last component is the function name.
- The next to last component is the python file name without the .py suffix.
- The preceding components identify parent folders. Folders should be
  relative to the current working directory which is typically the
  project root.

## color option

The --color -c option colors the --log pass/failed/error/skip status.

## style option

The --style option enables the PYPI project [Pygments][19] syntax
highlighting style used in the --log output.
The style option requires the `[color]` installation extra.

```txt
--style <pygments-style-name>
```

## Extend an example across files

Names assigned by all the blocks in a file can be shared, as global variables,
to files specified later in the command line.
Add a markdown file path to the --share-across-files command line option.
The 'shared' file(s) must also be specified as a FILE positional command line argument.

- [share demo](docs/share/share_demo.md) |
  [how it works](docs/codemode.md#share-across-files)
- [--replmode share demo](docs/repl/replshare_demo.md) |
  [how it works](docs/sessionmode.md#share-across-files)

## Skip blocks from the command line

The skip `--skip TEXT` command line option
prevents testing of any Python code or REPL block that contains the substring TEXT.
The block is logged as skip with `--skip TEXT` as the reason.

## summary option

The example  [here](docs/share/share_demo.md) shows --summary output.

## TOML configuration

Command line options can be augmented with values from a `[tool.phmutest]` section in
a .toml configuration file. It can be in a new file or added to an existing
.toml file like pyproject.toml.
The configuration file is specified by the `--config FILE` command line option.

Zero or more of these TOML keys may be present in the `[tool.phmutest]` section.

| TOML key           | Usage option        | TOML value - double quoted strings
| :------------------| :-----------------: | :---------:
| include-globs      | positional arg FILE | list of filename glob to select files
| exclude-globs      | positional arg FILE | list of filename glob to deselect files
| share-across-files | --share-across-files  | list of path
| setup-across-files | --setup-across-files  | list of path
| fixture            | --fixture           | dotted path
| select             | --select            | list of group directive name
| deselect           | --deselect          | list of group directive name
| color              | --color             | Use unquoted true to set
| style              | --style             | set Pygments syntax highlighting style

Only one of select and deselect can have strings.

- globs are described by Python standard library **pathlib.Path.glob()**.
- Any FILEs on the command line extend the files selected by include-globs and
  exclude-globs.
- Command line options supersede the keys in the config file.
- See the example **tests/toml/project.toml**.

## Run as a Python module

To run phmutest as a Python module:

```bash
python -m phmutest README.md --log
```

## Call from Python

Call **phmutest.main.command()** with a string that looks like a
command line less the phmutest, like this:
`"tests/md/project.md --replmode"`

- A `phmutest.summary.PhmResult` instance is returned.
- When calling from Python there is no shell wildcard expansion.
- **phmutest.main.main()** takes a list of strings like this:
  `["tests/md/project.md", "--replmode"]` and returns `phmutest.summary.PhmResult`.

[Example](docs/callfrompython.md) | [Limitation](docs/callfrompython.md#limitation)

## Patch points

Feel free to **unittest.mock.patch()** at these places in the code and not worry about
breakage in future versions. Look for examples in tests/test_patching.py.

### List of patch points

|       patched function              | purpose
| :--------------------------------:  | :----------:
| phmutest.direct.directive_finders() | Add directive aliases
| phmutest.fenced.python_matcher()    | Add detect Python from FCB info string
| phmutest.select.OUTPUT_INFO_STRINGS | Change detect expected output FCB info string
| phmutest.session.modify_docstring() | Inspect/modify REPL text before testing
| phmutest.reader.post()              | Inspect/modify DocNode detected in Markdown

## Hints

- Since phmutest generates code, the input files should be from a trusted
  source.
- The phmutest Markdown parser finds fenced code blocks enclosed by
  html `<details>` and `</details>` tags.
  The tags may require a preceding and trailing blank line
  to render correctly. See example at the bottom tests/md/readerfcb.md.
- Markdown indented code blocks ([Spec][4] section 4.4) are ignored.
- A malformed HTML comment ending is bad. Make sure
  it ends with both dashes like `-->`.
- A misspelled directive will be missing from the --report output.
- If the generated test file has a compile error phmutest will raise an
  ImportError when importing it.
- Blocks skipped with --skip and the phmutest-skip directive
  are not rendered. This is useful to avoid above import error.
- In repl mode **no** skipped blocks are rendered.
- "--quiet" is passed to the unittest test runner.
- The unittest "--locals" provides more information in traces.
- Try redirecting `--generate -` standard output into PYPI Pygments to
  colorize the generated test file.
- In code mode patches made by a fixture function are placed
  when the testfile is run.
- In code mode printing a class (not an instance) and then checking it in an
  expected-output FCB is not feasible because Python prints the
  `__qualname__`. See the file tests/md/qualname.md for an explanation.
- phmutest is implemented with non-thread-safe context managers.

## Related projects

- phmdoctest
- rundoc
- byexample
- sphinx.ext.doctest
- sybil
- doxec
- egtest
- pytest-phmdoctest
- pytest-codeblocks

## Differences between phmutest and phmdoctest

- phmutest treats each Markdown file as a single long example. phmdoctest
  tests each FCB in isolation. Adding a share-names directive is necessary to
  extend an example across FCBs within a file.
- Only phmutest can extend an example across files.
- phmutest uses Python standard library unittest and doctest as test runners.
  phmdoctest writes a pytest testfile for each Markdown file
  which requires a separate step to run. The testfiles then need to be discarded.
- phmdoctest offers two pytest fixtures that can be used in a pytest test case
  to generate and run a testfile in one step.
- phmutest generates tests for multiple Markdown files in one step
  and runs them internally so there are no leftover test files.
- The --fixture test suite initialization and cleanup is only available on phmutest.
  phmdoctest offers some initialization behavior using an FCB with a setup
  directive and its --setup-doctest option and it only works with sessions.
  See phmdoctest documentation "Execution Context"
  section for an explanation.
- phmutest does not support inline annotations.

[3]: https://github.github.com/gfm/#fenced-code-blocks
[4]: https://spec.commonmark.org
[5]: https://docs.python.org/3/library/doctest.html
[13]: https://ci.appveyor.com/project/tmarktaylor/phmutest
[17]: https://pypi.python.org/pypi/phmdoctest
[18]: https://docs.python.org/3/library/unittest.html
[19]: https://pypi.python.org/pypi/pygments
[20]: https://docs.pytest.org
[21]: https://github.com/cknd/stackprinter/blob/master/README.md

MIT License

Copyright (c) 2025 Mark Taylor

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

            

Raw data

            {
    "_id": null,
    "home_page": "https://phmutest.readthedocs.io/en/latest/",
    "name": "phmutest",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "documentation, markdown, testing",
    "author": "Mark Taylor",
    "author_email": "mark66547ta2@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/cc/3e/e3889a0226684272642ded41d57ca8ce72cc793ea934c404b318b22fb4a5/phmutest-1.0.0.tar.gz",
    "platform": null,
    "description": "# phmutest 1.0.0\n\n## Detect and troubleshoot broken Python examples in Markdown\n\n- Hybrid Python library / console program checks Python syntax highlighted examples.\n- Python tools to get fenced code block contents from Markdown. | [Here](docs/api.md)\n\nTreats each Markdown file as a single long example, which continues\nacross multiple Markdown [fenced code blocks][3] (FCBs or blocks).\n\n[Skip example and jump down to Features](#features)\n\n## A broken Example\n\nWhen tests fail we show what caused the error to help you quickly find the root cause.\nThis example shows how to use the example library answerlib\n| [answerlib.py](docs/answerlib_py.md).\nIt answers a question put to the ask method. | [phmutest output](#phmutest-console-output)\n\n```python\nfrom docs.answerlib import RightAnswer, WrongAnswer, RaiserBot\n```\n\nCreate a RightAnswer instance and ask a question.\nThe assert statement checks the answer.\nphmutest assigns a pass/failed/error/skip status to each Python FCB.\nThis FCB is given 'pass' status.\nNote how the example continues across multiple FCBs.\nIt continues for the entire Markdown file.\n\n### pass result\n\n```python\npass_bot = RightAnswer()\nanswer = pass_bot.ask(question=\"What floats?\")\nassert answer == \"apples\"\n```\n\n### failed result\n\nCreate a WrongAnswer instance and ask a question.\nThe WrongAnswer instance ask() method returns an\nincorrect answer.\nThe assert statement checks the answer,\nfinds that\nit is wrong and raises an AssertionError.\nThis FCB is given 'failed' status.\n\n```python\nfail_bot = WrongAnswer()\nanswer = fail_bot.ask(question=\"What floats?\")\nassert answer == \"apples\"\n```\n\n### error result\n\nNow we are going to cause the answerlib to raise an\nexception by calling the method inquire() which does not exist.\nThis raises an AttributeError in the library which propagates\nup and out of the first line of the FCB below.\nThis FCB is given 'error' status.\n\n```python\nanswer = pass_bot.inquire(query=\"What floats?\")\nassert answer == \"apples\"\n```\n\nThe test runner keeps going even after an exception. To stop\non first failure use the \"-f\" option.\n\n```python\nanswer = pass_bot.ask(question=\"What floats?\")\nassert answer == \"apples\"\n```\n\nCause another exception within answerlib to see the FCB line\nwhere the exception propagates out of the FCB in the log.\nThis FCB is also given 'error' status. See the results in the\nlog below.\n\n```python\nraiser_bot = RaiserBot()\n_ = raiser_bot.ask(question=\"What floats?\")\n```\n\n### Checking expected output\n\nAdd an FCB that immediately follows a Python code block that has no info string\nor the info string `expected-output`. Captured stdout is compared to the block.\nIn the log a \"o\" after the filename indicates expected output was checked.\n\n```python\nprint(\"Incorrect expected output.\")\n```\n\n```expected-output\nHello World!\n```\n\n### phmutest command line\n\n```shell\nphmutest README.md --log --quiet\n```\n\n### phmutest console output\n\nThere are two parts:\n\n- unittest printing to sys.stderr\n- phmutest printing to sys.stdout\n\n#### phmutest stdout\n\nThis shows the --log output.\nBelow the log table are the broken FCB Markdown source file lines.\n\n- The location is the file and line number of the opening fence of the FCB.\n- The \">\" indicates the line that raised the exception.\n\n```txt\nlog:\nargs.files: 'README.md'\nargs.log: 'True'\n\nlocation|label  result  reason\n--------------  ------  ---------------------------------------------------------------\nREADME.md:20..  pass\nREADME.md:33..  pass\nREADME.md:49..  failed  AssertionError\nREADME.md:63..  error   AttributeError: 'RightAnswer' object has no attribute 'inquire'\nREADME.md:71..  pass\nREADME.md:81..  error   ValueError: What was the question?\nREADME.md:92 o  failed\n--------------  ------  ---------------------------------------------------------------\n\nREADME.md:49\n    50  fail_bot = WrongAnswer()\n    51  answer = fail_bot.ask(question=\"What floats?\")\n>   52  assert answer == \"apples\"\n        AssertionError\n\nREADME.md:63\n>   64  answer = pass_bot.inquire(query=\"What floats?\")\n        AttributeError: 'RightAnswer' object has no attribute 'inquire'\n\nREADME.md:81\n    82  raiser_bot = RaiserBot()\n>   83  _ = raiser_bot.ask(question=\"What floats?\")\n        ValueError: What was the question?\n\nREADME.md:92\n    93  print(\"Incorrect expected output.\")\nAssertionError: 'Hello World!\\n' != 'Incorrect expected output.\\n'\n- Hello World!\n+ Incorrect expected output.\n```\n\nOn GitHub, to see Markdown line numbers, view this file and choose\nCode button. (Code is between Preview and Blame).\n\n##### traceback\n\nWhen phmutest is installed with the `[traceback]` extra,\na [stackprinter][21] formatted\ntraceback prints after each broken FCB. [Here](docs/traceback.md)\nis an example traceback.\n\n#### unittest stderr\n\nHere is the unittest output printed to sys.stderr.\nIt starts with captured stdout/stderr from the 'error' FCBs.\nMarkdown Python FCBs are copied to a temporary 'testfile' that is\nrun by the unittest test runner. The test runner prints to stderr before\nthe phmutest stdout printing. The test runner output provides tracebacks\nfor the assertions and exceptions.\nThe testfile line numbers will mostly be different than the Markdown\nline numbers. Look for the Markdown line numbers in the log. (Python 3.11)\n\n```txt\n=== README.md:81 stdout ===\nThis is RaiserBot.ask() on stdout answering 'What floats?'.\n=== end ===\n=== README.md:81 stderr ===\nThis is RaiserBot.ask() on stderr: Uh oh!\n=== end ===\n======================================================================\nERROR: tests (_phm1.Test001.tests) [README.md:63]\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"C:\\Users\\XXX\\AppData\\Local\\Temp\\YYY\\_phm1.py\", line 42, in tests\n    answer = pass_bot.inquire(query=\"What floats?\")\n             ^^^^^^^^^^^^^^^^\nAttributeError: 'RightAnswer' object has no attribute 'inquire'\n\n======================================================================\nERROR: tests (_phm1.Test001.tests) [README.md:81]\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"C:\\Users\\XXX\\AppData\\Local\\Temp\\YYY\\_phm1.py\", line 55, in tests\n    _ = raiser_bot.ask(question=\"What floats?\")\n        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"C:\\Users\\XXX\\Documents\\u0\\docs\\answerlib.py\", line 32, in ask\n    raise ValueError(\"What was the question?\")\nValueError: What was the question?\n\n======================================================================\nFAIL: tests (_phm1.Test001.tests) [README.md:49]\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"C:\\Users\\XXX\\AppData\\Local\\Temp\\YYY\\_phm1.py\", line 37, in tests\n    assert answer == \"apples\"\n           ^^^^^^^^^^^^^^^^^^\nAssertionError\n\n======================================================================\nFAIL: tests (_phm1.Test001.tests) [README.md:92]\n----------------------------------------------------------------------\nTraceback (most recent call last):\n  File \"C:\\Users\\XXX\\AppData\\Local\\Temp\\YYY\\_phm1.py\", line 66, in tests\n    _phm_testcase.assertEqual(_phm_expected_str, _phm_printer.stdout())\nAssertionError: 'Hello World!\\n' != 'Incorrect expected output.\\n'\n- Hello World!\n+ Incorrect expected output.\n\n\n----------------------------------------------------------------------\nRan 1 test in 0.003s\n\nFAILED (failures=2, errors=2)\n```\n\n### Features\n\n- Checks either Python code examples **or** \">>>\" REPL examples\n  | [doctest][5].\n- Reports pass/failed/error/skip status and line number for each block.\n- Shows block source indicating the line where the exception propagated.\n- Support for setup and cleanup. Acquire and release resources, change context,\n  Pass objects as global variables to the examples. Cleans up even when fail-fast.\n  [Suite initialization and cleanup](#suite-initialization-and-cleanup)\n- Write a pytest testfile into an existing pytest test suite.\n- Runs files in user specified order.\n- TOML configuration available.\n- An example can continue **across** files.\n- Show stdout printed by examples. --stdout\n- Colors pass/failed/error/skip status. --color.\n- Check expected output of code examples. Markdown edits are required.\n- Designated and stable **patch points** for Python standard library\n  **unittest.mock.patch()** patches. | [Here](#patch-points)\n\n### Advanced features\n\nThese features require adding tool specific HTML comment **directives**\nto the Markdown. Because directives are HTML comments they are not visible in\nrendered Markdown. View directives on GitHub\nby pressing the `Code` button in the banner at the top of the file.\n| [Advanced feature details](docs/advanced.md).\n\n- Assign test group names to blocks. Command line options select or\n  deselect test groups by name.\n- Skip blocks or skip checking printed output.\n- Label any fenced code block for later retrieval.\n- Accepts [phmdoctest][17] directives except share-names and clear-names.\n- Specify blocks as setup and teardown code for the file or setup across files.\n\n## main branch status\n\n[![license](https://img.shields.io/pypi/l/phmutest.svg)](https://github.com/tmarktaylor/phmutest/blob/main/LICENSE)\n[![pypi](https://img.shields.io/pypi/v/phmutest.svg)](https://pypi.python.org/pypi/phmutest)\n[![python](https://img.shields.io/pypi/pyversions/phmutest.svg)](https://pypi.python.org/pypi/phmutest)\n\n[![CI](https://github.com/tmarktaylor/phmutest/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tmarktaylor/phmutest/actions/workflows/ci.yml)\n[![Build status](https://ci.appveyor.com/api/projects/status/nbu1xlraoii8x377?svg=true)](https://ci.appveyor.com/project/tmarktaylor/phmutest)\n[![readthedocs](https://readthedocs.org/projects/phmutest/badge/?version=latest)](https://phmutest.readthedocs.io/en/latest/?badge=latest)\n[![codecov](https://codecov.io/gh/tmarktaylor/phmutest/coverage.svg?branch=main)](https://codecov.io/gh/tmarktaylor/phmutest?branch=main)\n\n[Docs RTD](https://phmutest.readthedocs.io/en/latest/) |\n[Docs GitHub](https://github.com/tmarktaylor/phmutest/blob/main/README.md) |\n[Repos](https://github.com/tmarktaylor/phmutest) |\n[pytest][13] |\n[Codecov](https://codecov.io/gh/tmarktaylor/phmutest?branch=main) |\n[License](https://github.com/tmarktaylor/phmutest/blob/main/LICENSE)\n\n[Installation](#installation) |\n[Usage](#usage) |\n[FILE](#file) |\n[REPL mode](#repl-mode) |\n[Suite initialization and cleanup](#suite-initialization-and-cleanup) |\n[--color](#color-option) |\n[--style](#style-option) |\n[Extend an example across files](#extend-an-example-across-files) |\n[Skip blocks from the command line](#skip-blocks-from-the-command-line) |\n[--summary](#summary-option) |\n[TOML configuration](#toml-configuration) |\n[Run as a Python module](#run-as-a-python-module) |\n[Call from Python](#call-from-python) |\n[Patch points](#patch-points) |\n[Hints](#hints) |\n[Related projects](#related-projects) |\n[Differences between phmutest and phmdoctest](#differences-between-phmutest-and-phmdoctest)\n\n[Sections](docs/demos.md#sections) |\n[Demos](docs/demos.md#demos) |\n[Changelog](CHANGELOG.md) |\n[Contributions](CONTRIBUTING.md)\n\nSee [list of demos](docs/demos.md)\nSee [How it works](docs/howitworks.md)\n\n## Installation\n\n```shell\npython -m pip install phmutest\n```\n\n- No required dependencies since Python 3.11. Depends on tomli before Python 3.11.\n- Pure Python. No binaries.\n- It is advisable to install in a virtual environment.\n\n### install extras\n\nThe extra 'color' enables the --color and\n--style options.\n\n```shell\npython -m pip install \"phmutest[color]\"  # Windows\npython -m pip install 'phmutest[color]'  # Unix/macOS\n```\n\nThe extra 'pytest' installs pytest and the plugin\npytest-subtests.\npytest-subtests continues running subtests after\nthe first subtest failure. [pytest][20] prints a very\nhelpful traceback when FCBs break.\n\n```shell\npython -m pip install \"phmutest[pytest]\"  # Windows\npython -m pip install 'phmutest[pytest]'  # Unix/macOS\n```\n\nThe extra 'traceback' enables [stackprinter][21] traceback\nprinting for each broken FCB. The traceback is\nslightly different than pytest's.\n\n```shell\npython -m pip install \"phmutest[traceback]\"  # Windows\npython -m pip install 'phmutest[traceback]'  # Unix/macOS\n```\n\nInstall with the extra 'dev' to install locally the same tools used by\nthe continuous integration scripts.\n\n```shell\npython -m pip install \"phmutest[dev]\"  # Windows\npython -m pip install 'phmutest[dev]'  # Unix/macOS\n```\n\nInstall with all the extras.\n\n```shell\npython -m pip install \"phmutest[color, traceback, dev]\"  # Windows\npython -m pip install 'phmutest[color, traceback, dev]'  # Unix/macOS\n```\n\n## Usage\n\n`phmutest --help`\n\n```txt\nusage: phmutest [-h] [--version] [--skip [TEXT ...]] [--fixture DOTTED_PATH.FUNCTION]\n                [--share-across-files [FILE ...]]\n                [--setup-across-files [FILE ...]] [--select [GROUP ...] | --deselect\n                [GROUP ...]] [--config TOMLFILE] [--replmode] [--color]\n                [--style STYLE] [-g OUTFILE]\n                [--progress] [--sharing [FILE ...]] [--log] [--summary] [--stdout] [--report]\n                [FILE ...]\n\nDetect and troubleshoot broken Python examples in Markdown. Accepts relevant unittest options.\n\npositional arguments:\n  FILE                  Markdown input file.\n\noptions:\n  -h, --help            show this help message and exit\n  --version             show program's version number and exit\n  --skip [TEXT ...]     Any block that contains the substring TEXT is not tested.\n  --fixture DOTTED_PATH.FUNCTION\n                        Function run before testing.\n  --share-across-files [FILE ...]\n                        Shares names from Markdown file to later positional files.\n  --setup-across-files [FILE ...]\n                        Apply Markdown file setup blocks to files.\n  --select [GROUP ...]  Select all blocks with phmutest-group GROUP directive for testing.\n  --deselect [GROUP ...]\n                        Exclude all blocks with phmutest-group GROUP directive from testing.\n  --config TOMLFILE     .toml configuration file.\n  --replmode            Test Python interactive sessions.\n  --color, -c           Enable --log pass/failed/error/skip result colors.\n  --style STYLE         Specify a Pygments style name as STYLE to enable syntax highlighting.\n  -g OUTFILE, --generate OUTFILE\n                        Write generated Python or docstring to output file or stdout.\n  --progress            Print block by block test progress. File by file in --replmode.\n  --sharing [FILE ...]  For these files print name sharing. . means all files.\n  --log                 Print log items when done.\n  --summary             Print test count and skipped tests.\n  --stdout              Print output printed by blocks.\n  --report              Print fenced code block configuration, deselected blocks.\n```\n\n- The **-f** option indicates fail fast.\n\n## FILE\n\nThe Markdown files are processed in the same order they are present as positional\narguments on the command line.\nShell wildcards can be used. Be aware that the shell expansion and operating system\nwill determine the order.\n\n## REPL mode\n\nWhen --replmode is specified Python interactive sessions are tested and\nPython code and expected output blocks are not tested. REPL mode tests are\nimplemented using [doctest][5].\nThe option --setup-across-files and the setup and teardown directives\nhave no effect in REPL mode.\n--progress has file by file granularity.\nSee the [Broken REPL example](docs/repl/REPLexample.md).\n\n## Suite initialization and cleanup\n\nFor background refer to definitions at the top of [unittest][18].\nUse --fixture to specify a Python initialization function that runs before the tests.\nIt works with or without --replmode, but there are differences.\nIn both modes, the fixture function may create objects (globs) that are visible\nas globals to the FCBs under test.\n\nIn the event of test errors orderly cleanup/release of resources is assured.\nFor Python code blocks the fixture may register cleanup functions by\ncalling **unittest.addModuleCleanup()**.\nIn REPL mode the fixture function optionally returns a cleanup function.\n\n- The fixture can acquire and release resources or change context.\n- The fixture can make entries to the log displayed by --log.\n- The fixture can install patches to the code under test.\n\nSpecify the --fixture function as\na relative **dotted path** where `/` is replaced with `.`.\nFor example, the function **my_init()** in the file **tests/myfixture.py**\nwould be specified:\n\n--fixture **tests.myfixture.my_init**\n\nThe function is passed keyword only arguments and **optionally**\nreturns a Fixture instance.\nThe keyword arguments and return type are described by\n[src/phmutest/fixture.py](docs/fixture_py.md).\nThe fixture file should be in the project directory tree. Fixture demos:\n\n- [fixture change workdir](docs/fix/code/chdir.md)\n- [fixture set globals](docs/fix/code/globdemo.md)\n- [fixture cleanup REPL Mode](docs/fix/repl/drink.md)\n\nThe test case test_doctest_optionflags_patch() shows an\nexample with a fixture that applies a patch to\ndoctest optionflags in --replmode.\n\n### Calling phmutest from pytest\n\nIn some of the tests the --fixture function is in the same pytest file as the\nphmutest library call.  This is not recommended because the Python file is\nimported again by fixture_function_importer() to a new module object.\nThe Python file's module level code will\nbe run a second time. If there are side-effects they will be repeated, likely\nwith un-desirable and hard to troubleshoot behavior.\n\n### Dotted path details\n\nThe fixture function must be at the top level of a .py file.\n\n- The dotted_path has components separated by \".\".\n- The last component is the function name.\n- The next to last component is the python file name without the .py suffix.\n- The preceding components identify parent folders. Folders should be\n  relative to the current working directory which is typically the\n  project root.\n\n## color option\n\nThe --color -c option colors the --log pass/failed/error/skip status.\n\n## style option\n\nThe --style option enables the PYPI project [Pygments][19] syntax\nhighlighting style used in the --log output.\nThe style option requires the `[color]` installation extra.\n\n```txt\n--style <pygments-style-name>\n```\n\n## Extend an example across files\n\nNames assigned by all the blocks in a file can be shared, as global variables,\nto files specified later in the command line.\nAdd a markdown file path to the --share-across-files command line option.\nThe 'shared' file(s) must also be specified as a FILE positional command line argument.\n\n- [share demo](docs/share/share_demo.md) |\n  [how it works](docs/codemode.md#share-across-files)\n- [--replmode share demo](docs/repl/replshare_demo.md) |\n  [how it works](docs/sessionmode.md#share-across-files)\n\n## Skip blocks from the command line\n\nThe skip `--skip TEXT` command line option\nprevents testing of any Python code or REPL block that contains the substring TEXT.\nThe block is logged as skip with `--skip TEXT` as the reason.\n\n## summary option\n\nThe example  [here](docs/share/share_demo.md) shows --summary output.\n\n## TOML configuration\n\nCommand line options can be augmented with values from a `[tool.phmutest]` section in\na .toml configuration file. It can be in a new file or added to an existing\n.toml file like pyproject.toml.\nThe configuration file is specified by the `--config FILE` command line option.\n\nZero or more of these TOML keys may be present in the `[tool.phmutest]` section.\n\n| TOML key           | Usage option        | TOML value - double quoted strings\n| :------------------| :-----------------: | :---------:\n| include-globs      | positional arg FILE | list of filename glob to select files\n| exclude-globs      | positional arg FILE | list of filename glob to deselect files\n| share-across-files | --share-across-files  | list of path\n| setup-across-files | --setup-across-files  | list of path\n| fixture            | --fixture           | dotted path\n| select             | --select            | list of group directive name\n| deselect           | --deselect          | list of group directive name\n| color              | --color             | Use unquoted true to set\n| style              | --style             | set Pygments syntax highlighting style\n\nOnly one of select and deselect can have strings.\n\n- globs are described by Python standard library **pathlib.Path.glob()**.\n- Any FILEs on the command line extend the files selected by include-globs and\n  exclude-globs.\n- Command line options supersede the keys in the config file.\n- See the example **tests/toml/project.toml**.\n\n## Run as a Python module\n\nTo run phmutest as a Python module:\n\n```bash\npython -m phmutest README.md --log\n```\n\n## Call from Python\n\nCall **phmutest.main.command()** with a string that looks like a\ncommand line less the phmutest, like this:\n`\"tests/md/project.md --replmode\"`\n\n- A `phmutest.summary.PhmResult` instance is returned.\n- When calling from Python there is no shell wildcard expansion.\n- **phmutest.main.main()** takes a list of strings like this:\n  `[\"tests/md/project.md\", \"--replmode\"]` and returns `phmutest.summary.PhmResult`.\n\n[Example](docs/callfrompython.md) | [Limitation](docs/callfrompython.md#limitation)\n\n## Patch points\n\nFeel free to **unittest.mock.patch()** at these places in the code and not worry about\nbreakage in future versions. Look for examples in tests/test_patching.py.\n\n### List of patch points\n\n|       patched function              | purpose\n| :--------------------------------:  | :----------:\n| phmutest.direct.directive_finders() | Add directive aliases\n| phmutest.fenced.python_matcher()    | Add detect Python from FCB info string\n| phmutest.select.OUTPUT_INFO_STRINGS | Change detect expected output FCB info string\n| phmutest.session.modify_docstring() | Inspect/modify REPL text before testing\n| phmutest.reader.post()              | Inspect/modify DocNode detected in Markdown\n\n## Hints\n\n- Since phmutest generates code, the input files should be from a trusted\n  source.\n- The phmutest Markdown parser finds fenced code blocks enclosed by\n  html `<details>` and `</details>` tags.\n  The tags may require a preceding and trailing blank line\n  to render correctly. See example at the bottom tests/md/readerfcb.md.\n- Markdown indented code blocks ([Spec][4] section 4.4) are ignored.\n- A malformed HTML comment ending is bad. Make sure\n  it ends with both dashes like `-->`.\n- A misspelled directive will be missing from the --report output.\n- If the generated test file has a compile error phmutest will raise an\n  ImportError when importing it.\n- Blocks skipped with --skip and the phmutest-skip directive\n  are not rendered. This is useful to avoid above import error.\n- In repl mode **no** skipped blocks are rendered.\n- \"--quiet\" is passed to the unittest test runner.\n- The unittest \"--locals\" provides more information in traces.\n- Try redirecting `--generate -` standard output into PYPI Pygments to\n  colorize the generated test file.\n- In code mode patches made by a fixture function are placed\n  when the testfile is run.\n- In code mode printing a class (not an instance) and then checking it in an\n  expected-output FCB is not feasible because Python prints the\n  `__qualname__`. See the file tests/md/qualname.md for an explanation.\n- phmutest is implemented with non-thread-safe context managers.\n\n## Related projects\n\n- phmdoctest\n- rundoc\n- byexample\n- sphinx.ext.doctest\n- sybil\n- doxec\n- egtest\n- pytest-phmdoctest\n- pytest-codeblocks\n\n## Differences between phmutest and phmdoctest\n\n- phmutest treats each Markdown file as a single long example. phmdoctest\n  tests each FCB in isolation. Adding a share-names directive is necessary to\n  extend an example across FCBs within a file.\n- Only phmutest can extend an example across files.\n- phmutest uses Python standard library unittest and doctest as test runners.\n  phmdoctest writes a pytest testfile for each Markdown file\n  which requires a separate step to run. The testfiles then need to be discarded.\n- phmdoctest offers two pytest fixtures that can be used in a pytest test case\n  to generate and run a testfile in one step.\n- phmutest generates tests for multiple Markdown files in one step\n  and runs them internally so there are no leftover test files.\n- The --fixture test suite initialization and cleanup is only available on phmutest.\n  phmdoctest offers some initialization behavior using an FCB with a setup\n  directive and its --setup-doctest option and it only works with sessions.\n  See phmdoctest documentation \"Execution Context\"\n  section for an explanation.\n- phmutest does not support inline annotations.\n\n[3]: https://github.github.com/gfm/#fenced-code-blocks\n[4]: https://spec.commonmark.org\n[5]: https://docs.python.org/3/library/doctest.html\n[13]: https://ci.appveyor.com/project/tmarktaylor/phmutest\n[17]: https://pypi.python.org/pypi/phmdoctest\n[18]: https://docs.python.org/3/library/unittest.html\n[19]: https://pypi.python.org/pypi/pygments\n[20]: https://docs.pytest.org\n[21]: https://github.com/cknd/stackprinter/blob/master/README.md\n\nMIT License\n\nCopyright (c) 2025 Mark Taylor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Detect and troubleshoot broken Python examples in Markdown.",
    "version": "1.0.0",
    "project_urls": {
        "Bug Reports": "https://github.com/tmarktaylor/phmutest/issues",
        "Homepage": "https://phmutest.readthedocs.io/en/latest/",
        "Source": "https://github.com/tmarktaylor/phmutest/"
    },
    "split_keywords": [
        "documentation",
        " markdown",
        " testing"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "b4b569cb9a68e92384ece0f61d15db3d50f1a878189fe1218ca4d8e826789564",
                "md5": "0e0a1ab5adb376eac26e4dbd7f9eabb3",
                "sha256": "68f563f950da71fa5ae9709b1dd460bfe782c1490d75e4f6bcc405deb2f94bb2"
            },
            "downloads": -1,
            "filename": "phmutest-1.0.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "0e0a1ab5adb376eac26e4dbd7f9eabb3",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 56439,
            "upload_time": "2025-02-08T21:50:42",
            "upload_time_iso_8601": "2025-02-08T21:50:42.272573Z",
            "url": "https://files.pythonhosted.org/packages/b4/b5/69cb9a68e92384ece0f61d15db3d50f1a878189fe1218ca4d8e826789564/phmutest-1.0.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "cc3ee3889a0226684272642ded41d57ca8ce72cc793ea934c404b318b22fb4a5",
                "md5": "f1d1e9f7bc140ab9d35681e67e75aa92",
                "sha256": "d8ab7eaadf80afc74c7254ca59e43e4ee98efe3bddb8676faab1fc875324a23b"
            },
            "downloads": -1,
            "filename": "phmutest-1.0.0.tar.gz",
            "has_sig": false,
            "md5_digest": "f1d1e9f7bc140ab9d35681e67e75aa92",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 139251,
            "upload_time": "2025-02-08T21:50:43",
            "upload_time_iso_8601": "2025-02-08T21:50:43.798917Z",
            "url": "https://files.pythonhosted.org/packages/cc/3e/e3889a0226684272642ded41d57ca8ce72cc793ea934c404b318b22fb4a5/phmutest-1.0.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-02-08 21:50:43",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "tmarktaylor",
    "github_project": "phmutest",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "appveyor": true,
    "requirements": [
        {
            "name": "tomli",
            "specs": [
                [
                    ">=",
                    "1.0.0"
                ]
            ]
        }
    ],
    "tox": true,
    "lcname": "phmutest"
}
        
Elapsed time: 3.30802s