{"id":34056767,"url":"https://github.com/kajaste/pychoir","last_synced_at":"2026-04-02T11:23:31.436Z","repository":{"id":43751202,"uuid":"318891522","full_name":"kajaste/pychoir","owner":"kajaste","description":"A Simple Unit Test Matcher Library for Python 3.6+","archived":false,"fork":false,"pushed_at":"2025-05-20T22:15:07.000Z","size":364,"stargazers_count":21,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-18T21:15:15.522Z","etag":null,"topics":["matcher","matchers","mocking","pytest","python","testing","unit-testing","unittest"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/pychoir/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kajaste.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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,"zenodo":null}},"created_at":"2020-12-05T21:20:22.000Z","updated_at":"2025-09-08T14:46:51.000Z","dependencies_parsed_at":"2023-12-09T02:30:04.942Z","dependency_job_id":"12abd4a3-9de1-4bdd-87a9-70f1c417e92e","html_url":"https://github.com/kajaste/pychoir","commit_stats":{"total_commits":119,"total_committers":4,"mean_commits":29.75,"dds":"0.32773109243697474","last_synced_commit":"19c3ca5cdf0baef25fe6303418c227a817c6a228"},"previous_names":[],"tags_count":30,"template":false,"template_full_name":null,"purl":"pkg:github/kajaste/pychoir","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kajaste%2Fpychoir","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kajaste%2Fpychoir/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kajaste%2Fpychoir/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kajaste%2Fpychoir/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kajaste","download_url":"https://codeload.github.com/kajaste/pychoir/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kajaste%2Fpychoir/sbom","scorecard":{"id":547958,"data":{"date":"2025-08-11","repo":{"name":"github.com/kajaste/pychoir","commit":"1089c0a6ec678127d9df24459868363002f8b77a"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.2,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/28 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":4,"reason":"5 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 4","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/python-package.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:28: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:32: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:44: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:48: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-package.yml:52: update your workflow using https://app.stepsecurity.io/secureworkflow/kajaste/pychoir/python-package.yml/main?enable=pin","Info:   0 out of  10 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-20T10:00:52.316Z","repository_id":43751202,"created_at":"2025-08-20T10:00:52.317Z","updated_at":"2025-08-20T10:00:52.317Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31305312,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T09:48:21.550Z","status":"ssl_error","status_checked_at":"2026-04-02T09:48:19.196Z","response_time":89,"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":["matcher","matchers","mocking","pytest","python","testing","unit-testing","unittest"],"created_at":"2025-12-14T03:03:21.852Z","updated_at":"2026-04-02T11:23:31.430Z","avatar_url":"https://github.com/kajaste.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `pychoir` - Python Test Matchers for humans\n[![PyPI version](http://img.shields.io/pypi/v/pychoir)](https://pypi.python.org/pypi/pychoir)\n[![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/pychoir.svg)](https://pypi.python.org/pypi/pychoir/)\n[![GitHub Actions (Tests)](https://github.com/kajaste/pychoir/workflows/Python%20package/badge.svg)](https://github.com/kajaste/pychoir)\n[![Documentation Status](https://readthedocs.org/projects/pychoir/badge/?version=stable)](https://pychoir.readthedocs.io/en/stable/?badge=latest)\n[![License](https://img.shields.io/pypi/l/pychoir.svg?style=flat)](https://github.com/kajaste/pychoir/blob/main/LICENSE.txt)\n\nSuper-duper low cognitive overhead matching for Python developers reading or writing tests. Implemented fully in modern \u0026 typed Python, without any dependencies. Runs and passes its tests on most Pythons after 3.6. PyPy works fine too.\n\n`pychoir` has mostly been developed for use with `pytest`, but nothing prevents from using it in any other test framework (like vanilla `unittest`) or even outside of testing, if you feel like it.\n\n## Installation\n* With pip: `pip install pychoir`\n* With pipenv: `pipenv install --dev pychoir`\n* With poetry: `poetry add --dev pychoir`\n* With PDM: `pdm add -dG test pychoir`\n* With uv: `uv add --dev pychoir`\n\n## Documentation\nCheck out the API Reference on readthedocs for detailed info and examples of all the available Matchers [https://pychoir.readthedocs.io/en/stable/api.html](https://pychoir.readthedocs.io/en/stable/api.html)\n\n## Why?\n\nYou have probably written quite a few tests where you assert something like\n\n```python\nassert thing_under_test() == {'some_fields': 'some values'}\n```\n\nHowever, sometimes you do not expect _exact_ equivalence. So you start\n\n```python\nresult = thing_under_test()\n\nresult_number = result.pop('number', None)\nassert result_number is None or result_number \u003c 3\n\nresult_list_of_strings = result.pop('list_of_strings', None)\nassert (\n    result_list_of_strings is not None\n    and len(result_list_of_strings) == 5\n    and all(isinstance(s, str) for s in result_list_of_strings)\n)\n\nassert result == {'some_fields': 'some values'}\n```\n\n...but this is not very convenient for anyone in the long run.\n\nThis is where `pychoir` comes in with matchers:\n\n```python\nfrom pychoir import LessThan, All, HasLength, IsNoneOr, IsInstance\n\nassert thing_under_test() == {\n    'number': IsNoneOr(LessThan(3)),\n    'list_of_strings': HasLength(5) \u0026 All(IsInstance(str)),\n    'some_fields': 'some values',\n}\n```\n\nIt can also be cumbersome to check mocked calls without using matchers:\n\n```python\nassert mock.call_args[0][0] \u003c 3\nassert isinstance(mock.call_args[0][1], str)\nassert len(mock.call_args[0][2]) == 3\n```\n\nbut simple and easy when using them:\n\n```python\nfrom pychoir import LessThan, IsInstance, HasLength\n\nmock.assert_called_with(LessThan(3), IsInstance(str), HasLength(3))\n```\n\nYou can also check many things about the same value: for example `IsInstance(int) \u0026 5` will make sure that the value is not only equal to 5, but is also an `int` (goodbye to accidental `5.0`).\n\nYou can place a matcher almost anywhere where a value can be. **`pychoir` matchers work well inside lists, tuples, dicts, dataclasses, mock call assertions...** You can also place normal values inside matchers, and they will match as with traditional `==` or `!=`.\n\nA core principle is that `pychoir` Matchers are composable and can be used freely in various combinations. For example `[LessThan(3) | 5]` is \"equal to\" a list with one item, holding a value equal to 5 or any value less than 3.\n\n## Can I write custom Matchers of my own\n\nYes, you can! `pychoir` Matcher baseclass has been designed to be usable by code outside the library. It also takes care of most of the generic plumbing, so your custom matcher typically needs very little code.\n\nHere is the implementation of `IsInstance` as an example:\n\n```python\nfrom typing import Any, Type\nfrom pychoir import Matcher\n\nclass IsInstance(Matcher):\n    def __init__(self, type_: Type[Any]):\n        super().__init__()\n        self.type = type_\n\n    def _matches(self, other: Any) -\u003e bool:\n        return isinstance(other, self.type)\n\n    def _description(self) -\u003e str:\n        return self.type.__name__\n\n```\n\nAll you need to take care of is defining the parameters (if any) in `__init__()`, the match itself in `_matches()`, and a description of the parameters in `_description()`.\n\nHere is an even simpler Anything matcher that does not take parameters and matches literally anything:\n\n```python\nfrom typing import Any\nfrom pychoir import Matcher\n\nclass Anything(Matcher):\n    def _matches(self, _: Any) -\u003e bool:\n        return True\n\n    def _description(self) -\u003e str:\n        return ''\n```\n\nIf your custom matcher is generic enough to be useful for everyone, please contribute (fork and make a pull request for now) and have it included in `pychoir`!\n\n## Why not \\\u003cX\\\u003e?\n\n### [PyHamcrest](https://github.com/hamcrest/PyHamcrest)\n\nNothing wrong with hamcrest as such, but `pychoir` aims to be better integrated with natural Python syntax, meaning for example that you do not need to use a custom assert function. `pychoir` matchers are drop-in replacements for your normal values alone or inside structures, even deeply nested ones. You can use hamcrest matchers through `pychoir` if you like, wrapping them in the `Matches(my_hamcrest_matcher)` matcher, although the idea is that `pychoir` would soon come with an equivalent set of matchers.\n\n### [assertpy](https://github.com/assertpy/assertpy)\n\nWhat a nice fluent API for matching, allowing matching multiple things at once. However, you can only match one value at a time. With `pychoir` you'll be matching the whole result at once, be it a single value, list, tuple, dict, dataclass, you name it. Let's see if `pychoir` gets some of that fluent stuff going forward as well.\n\n### ???\n\nI'd be happy to hear from you about other similar libraries.\n\n## What is it based on?\n\nPython has a rather peculiar way of handling equivalence, which allows customizing it in wild and imaginative ways. This is a very powerful feature, which one should usually avoid overusing. `pychoir` is built around the idea of using this power to build a lean and mean matcher implementation that looks like a custom DSL but is actually completely standard Python 3.\n\n## What is the project status?\n\n`pychoir` has quite a vast range of Matchers built in as well as basic API Reference documenting them. New ideas are still plenty and more can be discussed in [Discussions](https://github.com/kajaste/pychoir/discussions). Documentation will receive updates as well. Most remarkably fancy examples are missing. Making `pychoir` easier to contribute to is also on the list.\n\n## Where does the name come from?\n\nIt comes from the French word _pochoir_ which means a [drawing technique](https://fr.wikipedia.org/wiki/Pochoir) using templates. For some reason this method of matching in tests reminds me of drawing with those. A French word was chosen because it happens to start with a p and a vowel ;)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkajaste%2Fpychoir","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkajaste%2Fpychoir","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkajaste%2Fpychoir/lists"}