{"id":25290753,"url":"https://github.com/makukha/doctestcase","last_synced_at":"2026-02-17T18:04:02.477Z","repository":{"id":276533756,"uuid":"928984243","full_name":"makukha/doctestcase","owner":"makukha","description":"Evaluate doctests with configurable globals and `setUp`–`tearDown`. Export to Markdown and reST to include in docs.","archived":false,"fork":false,"pushed_at":"2025-04-03T15:24:06.000Z","size":182,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-27T19:05:59.785Z","etag":null,"topics":["doctest","markdown","restructuredtext","rst","test-case","unittest"],"latest_commit_sha":null,"homepage":"https://doctestcase.readthedocs.io","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/makukha.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-07T15:44:25.000Z","updated_at":"2025-04-03T15:24:06.000Z","dependencies_parsed_at":"2025-04-03T12:20:45.612Z","dependency_job_id":"44db58f6-0327-417c-886d-6668267e203a","html_url":"https://github.com/makukha/doctestcase","commit_stats":null,"previous_names":["makukha/doctestcase"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/makukha/doctestcase","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdoctestcase","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdoctestcase/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdoctestcase/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdoctestcase/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/makukha","download_url":"https://codeload.github.com/makukha/doctestcase/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdoctestcase/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29552252,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T17:56:56.811Z","status":"ssl_error","status_checked_at":"2026-02-17T17:56:55.544Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: 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":["doctest","markdown","restructuredtext","rst","test-case","unittest"],"created_at":"2025-02-13T00:49:52.083Z","updated_at":"2026-02-17T18:04:02.452Z","avatar_url":"https://github.com/makukha.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# doctestcase\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: exec yq '\"\u003e \" + .project.description' pyproject.toml --\u003e\n\u003e Evaluate doctests with configurable globals and `setUp`–`tearDown`. Export to Markdown and reST to include in docs.\n\u003c!-- docsub: end --\u003e\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include docs/badges.md --\u003e\n[![license](https://img.shields.io/github/license/makukha/doctestcase.svg)](https://github.com/makukha/doctestcase/blob/main/LICENSE)\n[![pypi](https://img.shields.io/pypi/v/doctestcase.svg#v0.2.2)](https://pypi.org/project/doctestcase)\n[![python versions](https://img.shields.io/pypi/pyversions/doctestcase.svg)](https://pypi.org/project/doctestcase)\n[![tests](https://raw.githubusercontent.com/makukha/doctestcase/v0.2.2/docs/img/badge/tests.svg)](https://github.com/makukha/doctestcase)\n[![coverage](https://raw.githubusercontent.com/makukha/doctestcase/v0.2.2/docs/img/badge/coverage.svg)](https://github.com/makukha/doctestcase)\n[![tested with multipython](https://img.shields.io/badge/tested_with-multipython-x)](https://github.com/makukha/multipython)\n[![uses docsub](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/makukha/docsub/refs/heads/main/docs/badge/v1.json)](https://github.com/makukha/docsub)\n[![mypy](https://img.shields.io/badge/type_checked-mypy-%231674b1)](http://mypy.readthedocs.io)\n[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/ruff)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10377/badge)](https://www.bestpractices.dev/projects/10377)\n\u003c!-- docsub: end --\u003e\n\n\n# Features\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include docs/features.md --\u003e\n* Combines `unittest.TestCase` and `doctest`\n* Inject globals into doctests\n* Mock doctests\n* Respect `setUp()` and `tearDown()`\n* Minimalistic decorator-based API\n* Format docstring as Markdown and reST to include in docs\n* Naturally fits [docsub](https://github.com/makukha/docsub)-based pipeline\n* No dependencies\n* Checked with mypy\n* 100% test coverage\n* Tested with Python 2.7+\n\u003c!-- docsub: end --\u003e\n\n\n# Installation\n\n```shell\n$ pip install doctestcase\n```\n\n\n# Usage\n\n\u003c!-- docsub: begin #docs.usage.md --\u003e\n\u003c!-- docsub: include docs/usage.md --\u003e\n* Decorated `TestCase`\n* Parametrize test case\n* Reuse `__doctestcase__` from other `TestCase`\n* Inherit from decorated `TestCase`\n* Format docstring as Markdown or reStructuredText\n* Integration with [docsub](https://github.com/makukha/docsub)\n\nSee [API Reference](https://doctestcase.readthedocs.io/en/latest/api.html) for details.\n\n\n### Decorated `TestCase`\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/simple.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n```python\nfrom doctest import ELLIPSIS\nfrom unittest import TestCase\n\nfrom doctestcase import doctestcase\n\n\n@doctestcase(globals={'X': 'yz'}, options=ELLIPSIS)\nclass SimpleCase(TestCase):\n    \"\"\"\n    Title\n\n    Paragraph.\n\n    \u003e\u003e\u003e X * 100\n    'yzyz...'\n\n    Another paragraph.\n\n    \u003e\u003e\u003e None\n    \u003e\u003e\u003e True\n    True\n    \"\"\"\n\n    def test_custom(self):  # called before 'test_docstring'\n        self.assertTrue(True)\n\n    def test_other(self):  # called after 'test_docstring'\n        self.assertTrue(True)\n```\n\u003c!-- docsub: end --\u003e\n\nAll test methods are called by `unittest` in alphabetic order, including `test_docstring`, added by `@doctestcase`.\n\n\n### Mock doctests\n\nIn classes decorated with `@doctestcase`, `unittest.mock` patches apply to doctests\ntoo, if patching is applied above `@doctestcase()` decorator. For Python below 3.3,\nuse [mock](https://pypi.org/project/mock/) package instead.\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/mock.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````python\nimport time\nfrom unittest import TestCase, mock\n\nfrom doctestcase import doctestcase\n\n\n@mock.patch('time.time', mock.MagicMock(return_value=0))\n@doctestcase()\nclass WithPatchedTime(TestCase):\n    \"\"\"\n    Mocking modules in doctests and testcase methods\n\n    \u003e\u003e\u003e import time\n    \u003e\u003e\u003e time.time()\n    0\n    \"\"\"\n\n    def test_method(self):\n        self.assertEqual(0, time.time())\n````\n\u003c!-- docsub: end --\u003e\n\n\n### Parametrize doctest case\n\nFirst, define base class parametrized with `cwd`:\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/param_base.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````python\nfrom doctest import ELLIPSIS\nimport os.path\nimport shutil\nimport tempfile\nfrom unittest import TestCase\n\nfrom doctestcase import doctestcase\n\n\n@doctestcase(options=ELLIPSIS, cwd='.')\nclass ChdirTestCase(TestCase):\n    def setUp(self):\n        if self.__class__ is ChdirTestCase:\n            self.skipTest('base class')  # no tests of the base class itself\n        self.temp = tempfile.mkdtemp()\n        self.prev = os.getcwd()\n        cwd = os.path.join(self.temp, self.__doctestcase__.kwargs['cwd'])\n        if not os.path.exists(cwd):\n            os.mkdir(cwd)\n        os.chdir(cwd)\n\n    def tearDown(self):\n        os.chdir(self.prev)\n        shutil.rmtree(self.temp)\n````\n\u003c!-- docsub: end --\u003e\n\nNotice how the base class is skipped from testing.\n\nIn this example we use `os.path` module for compatibility with older Python versions only. If you use recent Python versions, use `pathlib` instead.\n\nNow we can define test case parametrized with `cwd`:\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/param_child.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````python\n@doctestcase(cwd='subdir')\nclass Case1(ChdirTestCase):\n    \"\"\"\n    \u003e\u003e\u003e import os\n    \u003e\u003e\u003e os.getcwd()\n    '.../subdir'\n    \"\"\"\n````\n\u003c!-- docsub: end --\u003e\n\n\n### Reuse `__doctestcase__` from other `TestCase`\n\nExtending example above,\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/reuse.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n```python\n@SimpleCase.__doctestcase__\nclass AnotherCase(TestCase):\n    \"\"\"\n    Title\n\n    \u003e\u003e\u003e X * 100\n    'yzyz...'\n    \"\"\"\n```\n\u003c!-- docsub: end --\u003e\n\nNow `AnotherCase.__doctestcase__` holds shallow copy of `globals`, `kwargs`, and same doctest options, as `SimpleCase`. These copies are independent.\n\n\n### Inherit from decorated `TestCase`\n\nTest cases, decorated with `@doctestcase`, can be used as base classes for other test cases. This is useful when inherited classes need to extend or change properties, passed to parent's `@doctestcase`. Parent properties will be copied and updated with values from child class decorator.\n\nFor the `SimpleCase` class above,\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/inherit.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````python\n@doctestcase(globals={'A': 'bc'})\nclass InheritedCase(SimpleCase):\n    \"\"\"\n    Title\n\n    \u003e\u003e\u003e (X + A) * 100\n    'yzbcyzbc...'\n    \"\"\"\n````\n\u003c!-- docsub: end --\u003e\n\nNotice that global variable `A` was added to `globals` defined in `SimpleCase`, and the new class reuses `doctest.ELLIPSIS` option.\n\nFor more details on how `doctestcase` properties are updated, check the [API Reference](https://doctestcase.readthedocs.io/en/latest/api.html).\n\n\n### Format docstring as Markdown or reStructuredText\n\nFor the `SimpleCase` class above,\n\n#### Markdown\n\n```pycon\n\u003e\u003e\u003e from doctestcase import to_markdown\n\u003e\u003e\u003e to_markdown(SimpleCase)\n```\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/simple.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````markdown\n## Title\n\nParagraph.\n\n```pycon\n\u003e\u003e\u003e X * 100\n'yzyz...'\n```\n\nAnother paragraph.\n\n```pycon\n\u003e\u003e\u003e None\n\u003e\u003e\u003e True\nTrue\n```\n````\n\u003c!-- docsub: end --\u003e\n\n#### reStructuredText\n\n```pycon\n\u003e\u003e\u003e from doctestcase import to_rest\n\u003e\u003e\u003e to_rest(SimpleCase)\n```\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/usage/simple.rst --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````restructuredtext\nTitle\n-----\n\nParagraph.\n\n\u003e\u003e\u003e X * 100\n'yzyz...'\n\nAnother paragraph.\n\n\u003e\u003e\u003e None\n\u003e\u003e\u003e True\nTrue\n````\n\u003c!-- docsub: end --\u003e\n\n\n### Integration with [docsub](https://github.com/makukha/docsub)\n\nWhen documenting packages, \"Usage\" section often includes doctests. It is a good practice to test all documented use cases, so why not adopt test-driven documenting approach and write tests with docs in mind?\n\n1. Write tests with carefully crafted docstrings using doctests.\n2. Include generated Markdown or reST in docs.\n\nWith [docsub](https://github.com/makukha/docsub), this can be achieved with some minimal configuration.\n\nJust two commands to run tests and update docs:\n\n```shell\n$ pytest tests\n$ docsub sync -i usage.md\n```\n\n#### usage.md\n\n\u003c!-- docsub: begin #usage.md --\u003e\n\u003c!-- docsub: include tests/docsub/__result__.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````markdown\n# Usage\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: x case tests/test_usage.py:UseCase1 --\u003e\n## Use Case 1\n\nLong description of the use case.\n\nUsage example in doctest:\n\n```pycon\n\u003e\u003e\u003e True\nTrue\n```\n\u003c!-- docsub: end --\u003e\n````\n\u003c!-- docsub: end #usage.md --\u003e\n\n#### tests/test_usage.py\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/docsub/tests/test_usage.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````python\nfrom unittest import TestCase\n\nfrom doctestcase import doctestcase\n\n\n@doctestcase()\nclass UseCase1(TestCase):\n    \"\"\"\n    Use Case 1\n\n    Long description of the use case.\n\n    Usage example in doctest:\n\n    \u003e\u003e\u003e True\n    True\n    \"\"\"\n````\n\u003c!-- docsub: end --\u003e\n\n#### docsubfile.py\n\nDocsub configuration file declaring project-local x-tension command:\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/docsub/docsubfile.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````python\nfrom docsub import click\nfrom doctestcase import to_markdown\nfrom importloc import Location\n\n\n@click.group()\ndef x() -\u003e None:\n    pass\n\n\n@x.command()\n@click.argument('case')\ndef case(case: str) -\u003e None:\n    text = to_markdown(Location(case).load(), title_depth=2)\n    click.echo(text, nl=False)\n````\n\u003c!-- docsub: end --\u003e\n\u003c!-- docsub: end #docs.usage.md --\u003e\n\n\n# Contributing\n\nPull requests, feature requests, and bug reports are welcome!\n\n* [Contribution guidelines](https://github.com/makukha/doctestcase/blob/main/.github/CONTRIBUTING.md)\n\n\n# Authors\n\n* Michael Makukha\n\n\n# See also\n\n* [Documentation](https://doctestcase.readthedocs.io)\n* [Issues](https://github.com/makukha/doctestcase/issues)\n* [Changelog](https://github.com/makukha/doctestcase/blob/main/CHANGELOG.md)\n* [Security Policy](https://github.com/makukha/doctestcase/blob/main/.github/SECURITY.md)\n* [Contribution Guidelines](https://github.com/makukha/doctestcase/blob/main/.github/CONTRIBUTING.md)\n* [Code of Conduct](https://github.com/makukha/doctestcase/blob/main/.github/CODE_OF_CONDUCT.md)\n* [License](https://github.com/makukha/doctestcase/blob/main/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmakukha%2Fdoctestcase","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmakukha%2Fdoctestcase","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmakukha%2Fdoctestcase/lists"}