{"id":37607489,"url":"https://github.com/flyingcircusio/pytest-patterns","last_synced_at":"2026-01-16T10:13:39.326Z","repository":{"id":207819874,"uuid":"720125765","full_name":"flyingcircusio/pytest-patterns","owner":"flyingcircusio","description":"pytest-patterns is a plugin for pytest that provides a pattern matching engine optimized for testing.","archived":false,"fork":false,"pushed_at":"2024-04-11T19:13:08.000Z","size":44,"stargazers_count":25,"open_issues_count":2,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-04-12T00:18:27.461Z","etag":null,"topics":["pytest-plugin","python","testing"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flyingcircusio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-17T16:21:40.000Z","updated_at":"2024-02-07T16:28:56.000Z","dependencies_parsed_at":"2023-11-17T20:09:47.948Z","dependency_job_id":"e53acb46-a31e-429a-a152-ef586f9601ec","html_url":"https://github.com/flyingcircusio/pytest-patterns","commit_stats":{"total_commits":13,"total_committers":2,"mean_commits":6.5,"dds":0.3846153846153846,"last_synced_commit":"b2863d25f3beb36c8e3d0e1c52e51848c6f92c05"},"previous_names":["flyingcircusio/pytest-textmatch-experiment"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/flyingcircusio/pytest-patterns","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flyingcircusio%2Fpytest-patterns","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flyingcircusio%2Fpytest-patterns/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flyingcircusio%2Fpytest-patterns/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flyingcircusio%2Fpytest-patterns/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flyingcircusio","download_url":"https://codeload.github.com/flyingcircusio/pytest-patterns/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flyingcircusio%2Fpytest-patterns/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28478049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pytest-plugin","python","testing"],"created_at":"2026-01-16T10:13:39.235Z","updated_at":"2026-01-16T10:13:39.311Z","avatar_url":"https://github.com/flyingcircusio.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"`pytest-patterns` is a plugin for pytest that provides a pattern matching\nengine optimized for testing.\n\nBenefits:\n\n* provides easy to read reporting for complex patterns in long strings (1000+ lines)\n* assists in reasoning which patterns have matched or not matched – and why\n* can deal with ambiguity, optional and repetitive matches, intermingled\n  output from non-deterministic concurrent processes\n* helps writing patterns that are easy to read, easy to maintain and\n  easy to adjust in the face of unstable outputs\n* helps reusing patterns using pytest fixtures\n\nLong term goals:\n\nSupport testing of CLI output, as well as HTML and potentially typical text/*\ntypes like JSON, YAML, and others.\n\n# Examples\n\nTry and play around using the examples in the source repository. The\nexamples fail on purpose, because the failure reporting is the most important\nand useful part – aside from making it easier to write the assertions.\n\n```shell\n$ nix develop\n$ hatch run test examples -vv\n```\n\n# Basic API\n\n## `optional` matches and variability with the ellipsis `...`\n\nIf you want to test a complex string, start by pulling in the `patterns` fixture\nand create a named pattern that accepts any number of lines that contain\nthe word \"better\":\n\n```python\nimport this\n\nzen = \"\".join([this.d.get(c, c) for c in this.s])\n\n\ndef test_zen(patterns):\n    p = patterns.better_things\n    p.optional(\"...better...\")\n    assert p == zen\n```\n\nThis will not give us a green bar, as the pattern does match some lines, but\nsome lines were not matched and thus considered **unexpected**:\n\n```\n  🟢=EXPECTED | ⚪️=OPTIONAL | 🟡=UNEXPECTED | 🔴=REFUSED/UNMATCHED\n\n  Here is the string that was tested:\n\n  🟡                 | The Zen of Python, by Tim Peters\n  🟡                 |\n  ⚪️ better_things   | Beautiful is better than ugly.\n  ⚪️ better_things   | Explicit is better than implicit.\n  ⚪️ better_things   | Simple is better than complex.\n  ⚪️ better_things   | Complex is better than complicated.\n  ⚪️ better_things   | Flat is better than nested.\n  ⚪️ better_things   | Sparse is better than dense.\n  🟡                 | Readability counts.\n  🟡                 | Special cases aren't special enough to break the rules.\n  🟡                 | Although practicality beats purity.\n  🟡                 | Errors should never pass silently.\n  🟡                 | Unless explicitly silenced.\n  🟡                 | In the face of ambiguity, refuse the temptation to guess.\n  🟡                 | There should be one-- and preferably only one --obvious way to do it.\n  🟡                 | Although that way may not be obvious at first unless you're Dutch.\n  ⚪️ better_things   | Now is better than never.\n  ⚪️ better_things   | Although never is often better than *right* now.\n  🟡                 | If the implementation is hard to explain, it's a bad idea.\n  🟡                 | If the implementation is easy to explain, it may be a good idea.\n  🟡                 | Namespaces are one honking great idea -- let's do more of those!\n```\n\nThe report highlights which lines were matched (and which pattern caused the\nmatch) and are fine the way they are. Optional matches are displayed with a\nwhite circle (⚪️). The report also highlights those lines that weren't matched\nand marked with a yellow circle (🟡).\n\nWe know the Zen is correct the way it is, so lets use more of the API to continue\ncompleting the pattern.\n\n## `continous` matches\n\nLets use the `continuous` match which requires lines to come both in a specific order\nand must not be interrupted by other lines. We create a new named pattern and\n`merge` it with the previous pattern:\n\n```python\ndef test_zen(patterns):\n    ...\n\n    p = patterns.conclusio\n    p.continous(\"\"\"\nIf the implementation is hard to explain, it's a bad idea.\nIf the implementation is easy to explain, it may be a good idea.\nNamespaces are one honking great idea -- let's do more of those!\n\"\"\")\n    full_pattern = patterns.full\n    full_pattern.merge(\"better_things\", \"conclusio\")\n\n    assert full_pattern == zen\n```\n\nThis gets us a bit further:\n\n```\n  🟢=EXPECTED | ⚪️=OPTIONAL | 🟡=UNEXPECTED | 🔴=REFUSED/UNMATCHED\n\n  Here is the string that was tested:\n\n  🟡                 | The Zen of Python, by Tim Peters\n  🟡                 |\n  ⚪️ better_things   | Beautiful is better than ugly.\n  ⚪️ better_things   | Explicit is better than implicit.\n  ⚪️ better_things   | Simple is better than complex.\n  ⚪️ better_things   | Complex is better than complicated.\n  ⚪️ better_things   | Flat is better than nested.\n  ⚪️ better_things   | Sparse is better than dense.\n  🟡                 | Readability counts.\n  🟡                 | Special cases aren't special enough to break the rules.\n  🟡                 | Although practicality beats purity.\n  🟡                 | Errors should never pass silently.\n  🟡                 | Unless explicitly silenced.\n  🟡                 | In the face of ambiguity, refuse the temptation to guess.\n  🟡                 | There should be one-- and preferably only one --obvious way to do it.\n  🟡                 | Although that way may not be obvious at first unless you're Dutch.\n  ⚪️ better_things   | Now is better than never.\n  ⚪️ better_things   | Although never is often better than *right* now.\n  🟢 conclusio       | If the implementation is hard to explain, it's a bad idea.\n  🟢 conclusio       | If the implementation is easy to explain, it may be a good idea.\n  🟢 conclusio       | Namespaces are one honking great idea -- let's do more of those!\n```\n\nNote, that lines matched by `continuous` are highlighted in green as they are\nconsidered a stronger match than the `optional` ones.\n\n## `in_order` matches\n\nThere is still stuff missing. Lets make the test green by creating a match for\nall other lines using `in_order`, which expects the lines to come in the order\ngiven, but might be mixed in with other lines.\n\n```python\ndef test_zen(patterns):\n    ...\n    p = patterns.top_and_middle\n    p.in_order(\n        \"\"\"\nThe Zen of Python, by Tim Peters\n\nReadability counts.\nSpecial cases aren't special enough to break the rules.\nAlthough practicality beats purity.\nErrors should never pass silently.\nUnless explicitly silenced.\nIn the face of ambiguity, refuse the temptation to guess.\nThere should be one-- and preferably only one --obvious way to do it.\nAlthough that way may not be obvious at first unless you're Dutch.\n\"\"\"\n    )\n    full_pattern = patterns.full\n    full_pattern.merge(\"top_and_middle\", \"better_things\", \"conclusio\")\n\n    assert full_pattern == zen\n```\n\nShouldn't that have given us a green bar? I can still see a yellow circle there!\n\n```\n  🟢=EXPECTED | ⚪️=OPTIONAL | 🟡=UNEXPECTED | 🔴=REFUSED/UNMATCHED\n\n  Here is the string that was tested:\n\n  🟢 top_and_middle  | The Zen of Python, by Tim Peters\n  🟡                 |\n  ⚪️ better_things   | Beautiful is better than ugly.\n  ⚪️ better_things   | Explicit is better than implicit.\n  ⚪️ better_things   | Simple is better than complex.\n  ⚪️ better_things   | Complex is better than complicated.\n  ⚪️ better_things   | Flat is better than nested.\n  ⚪️ better_things   | Sparse is better than dense.\n  🟢 top_and_middle  | Readability counts.\n  🟢 top_and_middle  | Special cases aren't special enough to break the rules.\n  🟢 top_and_middle  | Although practicality beats purity.\n  🟢 top_and_middle  | Errors should never pass silently.\n  🟢 top_and_middle  | Unless explicitly silenced.\n  🟢 top_and_middle  | In the face of ambiguity, refuse the temptation to guess.\n  🟢 top_and_middle  | There should be one-- and preferably only one --obvious way to do it.\n  🟢 top_and_middle  | Although that way may not be obvious at first unless you're Dutch.\n  ⚪️ better_things   | Now is better than never.\n  ⚪️ better_things   | Although never is often better than *right* now.\n  🟢 conclusio       | If the implementation is hard to explain, it's a bad idea.\n  🟢 conclusio       | If the implementation is easy to explain, it may be a good idea.\n  🟢 conclusio       | Namespaces are one honking great idea -- let's do more of those!\n```\n\n## Handling empty lines with the `\u003cempty-line\u003e` marker\n\nThe previous pattern is not quite perfect because `pytest-patterns` has a\nspecial way to handle newlines both in patterns and in content.\n\n1. In content that is tested we never implicitly accept any lines that were not\n   specified, including empty lines.\n\n2. However, in patterns empty lines are not significant to allow you to use them\n   to make your patterns more readable by grouping lines visually.\n\nWe get out of this by using the special marker `\u003cempty-line\u003e` in our patterns\nwhich will match both literally for lines containing `\u003cempty-line\u003e` and which\nare empty lines:\n\n```python\ndef test_zen(patterns):\n    ...\n    p = patterns.top_and_middle\n    p.optional(\"\u003cempty-line\u003e\")\n    p.in_order(\n        \"\"\"\nThe Zen of Python, by Tim Peters\n\nReadability counts.\nSpecial cases aren't special enough to break the rules.\nAlthough practicality beats purity.\nErrors should never pass silently.\nUnless explicitly silenced.\nIn the face of ambiguity, refuse the temptation to guess.\nThere should be one-- and preferably only one --obvious way to do it.\nAlthough that way may not be obvious at first unless you're Dutch.\n\"\"\"\n    )\n    ...\n    assert full_pattern == zen\n```\n\nAnd we finally get a green bar:\n\n```\nexamples/test_examples.py::test_zen_5_1 PASSED\n```\n\n## `refused` matches\n\nUp until now we only created matches that allowed us to write down\nthings that we expect. However, we can also explicitly refuse lines - for example any line\ncontaining the word \"should\":\n\n```python\ndef test_zen(patterns):\n    ...\n    p = patterns.no_should\n    p.refused(\"...should...\")\n\n    full_pattern = patterns.full\n    full_pattern.merge(\n        \"no_should\", \"top_and_middle\", \"better_things\", \"conclusio\"\n    )\n\n    assert full_pattern == zen\n```\n\nThis is were `pytest-patterns` really shines. We now can quickly see which parts\nof our output is OK and which isn't and why:\n\n```\n  🟢=EXPECTED | ⚪️=OPTIONAL | 🟡=UNEXPECTED | 🔴=REFUSED/UNMATCHED\n\n  Here is the string that was tested:\n\n  🟢 top_and_middle  | The Zen of Python, by Tim Peters\n  🟡                 |\n  ⚪️ better_things   | Beautiful is better than ugly.\n  ⚪️ better_things   | Explicit is better than implicit.\n  ⚪️ better_things   | Simple is better than complex.\n  ⚪️ better_things   | Complex is better than complicated.\n  ⚪️ better_things   | Flat is better than nested.\n  ⚪️ better_things   | Sparse is better than dense.\n  🟢 top_and_middle  | Readability counts.\n  🟢 top_and_middle  | Special cases aren't special enough to break the rules.\n  🟢 top_and_middle  | Although practicality beats purity.\n  🔴 no_should       | Errors should never pass silently.\n  🟢 top_and_middle  | Unless explicitly silenced.\n  🟢 top_and_middle  | In the face of ambiguity, refuse the temptation to guess.\n  🔴 no_should       | There should be one-- and preferably only one --obvious way to do it.\n  🟢 top_and_middle  | Although that way may not be obvious at first unless you're Dutch.\n  ⚪️ better_things   | Now is better than never.\n  ⚪️ better_things   | Although never is often better than *right* now.\n  🟢 conclusio       | If the implementation is hard to explain, it's a bad idea.\n  🟢 conclusio       | If the implementation is easy to explain, it may be a good idea.\n  🟢 conclusio       | Namespaces are one honking great idea -- let's do more of those!`\n\n  These are the matched refused lines:\n\n  🔴 no_should       | ...should...\n  🔴 no_should       | ...should...\n\n```\n\n## Re-using patterns with fixtures\n\nLastly, we'd like to re-use patterns in multiple tests. Let's refactor the\ncurrent patterns into a separate fixture that can be activated as needed:\n\n```python\nimport pytest\nimport this\n\nzen = \"\".join([this.d.get(c, c) for c in this.s])\n\n\n@pytest.fixture\ndef zen_patterns(patterns):\n    p = patterns.better_things\n    p.optional(\"...better...\")\n\n    p = patterns.conclusio\n    p.continuous(\n        \"\"\"\\\nIf the implementation is hard to explain, it's a bad idea.\nIf the implementation is easy to explain, it may be a good idea.\nNamespaces are one honking great idea -- let's do more of those!\n\"\"\"\n    )\n\n    p = patterns.top_and_middle\n    p.in_order(\n        \"\"\"\nThe Zen of Python, by Tim Peters\n\nReadability counts.\nSpecial cases aren't special enough to break the rules.\nAlthough practicality beats purity.\nErrors should never pass silently.\nUnless explicitly silenced.\nIn the face of ambiguity, refuse the temptation to guess.\nThere should be one-- and preferably only one --obvious way to do it.\nAlthough that way may not be obvious at first unless you're Dutch.\n\"\"\"\n    )\n\n\ndef test_zen(patterns, zen_patterns):\n    full_pattern = patterns.full\n    full_pattern.merge(\"better_things\", \"conclusio\", \"top_and_middle\")\n\n    assert full_pattern == zen\n```\n\n## Handling tabs and whitespace\n\nWhen copying and pasting output from commands its easy to turn tabs from an\noriginal source into spaces and then accidentally not aligning things right.\n\nAs its more typical to not insert tabs in your code pytest-patterns converts\ntabs to spaces (properly aligned to 8 character stops as terminals render them):\n\n```python\ndef test_tabs_and_spaces(patterns):\n    data = \"\"\"\npre\u003e\\taligned text\nprefix\u003e\\tmore aligned text\n\"\"\"\n    tabs = patterns.tabs\n    tabs.in_order(\"\"\"\npre\u003e    aligned text\nprefix\u003e aligned text\n\"\"\")\n    assert tabs == data\n```\n\n# Development\n\n\n```shell\n$ pre-commit install\n$ nix develop\n$ hatch run test\n```\n\n\n# TODO\n\n* [ ] normalization feature\n\n    -\u003e json (+whitespace)\n        -\u003e python object causes serialization\n        -\u003e json object causes deserialization + reserialization (mit readable oder so)\n        -\u003e whitespace normalization\n\n    -\u003e html (+whitespace)\n        -\u003e parse html, then serialize in a normalized way\n        -\u003e whitespace normalization for both pattern and tested content\n\n    -\u003e whitespace (pattern and tested content)\n        -\u003e strip whitespace at beginning and end\n        -\u003e fold multiple spaces into single spaces (makes it harder to diagnose things)\n\n        -\u003e make tab replacement and format with control pictures optional.\n        -\u003e also, tab replacement only happens on the input line, not the test line because due to `...` we can't know where the tab will land\n\n* [ ] proper release process with tagging, version updates, etc.\n\n* [ ] Get coverage working correctly (https://pytest-cov.readthedocs.io/en/latest/plugins.html doesnt seem to help ...)\n\n* [ ] Get the project fully set up to make sense for interested parties and\n      potential contributors.\n\n* [ ] optional reporting without colors\n\n* [ ] matrix builds for multiple python versions / use tox locally and in github action?\n\n* [ ] highlight whitespace (e.g. \u003cTAB\u003e \u003cSPACE\u003e ) when reporting unmatched expected lines. this can be confusing if you see an \"empty\" line because you typoed e.g.:\n\n```\n    outmigrate.optional(\n        \"\"\"\nsimplevm             waiting                        interval=3 remaining=...\nsimplevm             check-staging-config           result='none'\nsimplevm             query-migrate                  arguments={} id=None subsystem='qemu/qmp'\nsimplevm             migration-status               mbps=... remaining='...' status='active'\nsimplevm             vm-destroy-kill-vm             attempt=... subsystem='qemu'\n    \"\"\"\n    )\n```\n\nDo you see it? There are four spaces on the last line which is now an expected line with four spaces ...\n\nThis could also be improved by ignoring whitespace only lines (optionally?)\n\n\n# DONE\n\n\n* [x] coding style template\n\n* [x] Actual documentation\n\n* [x] differentiate between tolerated and expected in status reporting\n\n* [x] add avoided lines that must not appear\n\n* [x] allow patterns expectations/tolerations/... to have names and use those to mark up the report why things matched?\n\n    * [T] DEBUG     | ....\n    * [X] MIGRATION |\n\n* [x] how to deal with HTML boilerplate -\u003e use `optional(\"...\")`\n\n* [x] add  lines that must appear in order without being interrupted\n\n# Later\n\n* [ ] html normalization might want to include a feature to suppress reporting\n   of certain lines (and just add `...` in the reporting output, e.g. if something\n   fails do not report the owrap lines\n\n* [ ] add line numbers\n\n* [ ] report line numbers on matched avoidances\n\n* [ ] structlog integration\n\n* [ ] more comprehensive docs\n\n# Wording\n\n\nMatches on patterns are adjectives:\n\n* These lines must appear and they must be **continuous**.\n* These lines must appear and they must be **in order**.\n* These lines are **optional**.\n* If those lines appear they are **refused**.\n\nModifiers to the pattern itself are verbs:\n\n* *Merge* the rules from those patterns into this one.\n* *Normalize* the input (and the rules) in this way.s\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflyingcircusio%2Fpytest-patterns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflyingcircusio%2Fpytest-patterns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflyingcircusio%2Fpytest-patterns/lists"}