CodeVetter · Repo Unpack · Contributor handoff
microsoft/markitdown
Edit surface is small and regular: to add/change a format, touch one file in converters/ plus tests. The invariants that matter: accepts() must leave the stream position unchanged; converters must raise MissingDependencyException (not ImportError) for optional deps; registration order is reversed within a priority. Run tests per-package (hatch test or pytest). Avoid touching _markitdown.py's _convert loop casually — many subtle contracts (position asserts, guess iteration, legacy kwargs) live there.
- To add a format: create converters/_foo_converter.py with ACCEPTED_MIME_TYPE_PREFIXES/ACCEPTED_FILE_EXTENSIONS constants and a DocumentConverter subclass, export it in converters/__init__.py, register it in MarkItDown.enable_builtins at the right spot, and add the extra to pyproject [project.optional-dependencies].
- Optional-dependency convention: wrap the import in try/except ImportError storing sys.exc_info() in a module-level _dependency_exc_info, then raise MissingDependencyException(MISSING_DEPENDENCY_MESSAGE.format(converter=..., extension=..., feature=...)) inside convert().
- Hard invariant from _base_converter docstring and _convert asserts: if accepts() reads from file_stream it MUST seek back; a converter that violates this triggers AssertionError inside _convert.
- Registration order is counterintuitive: register_converter inserts at index 0, so LATER registrations are tried FIRST among equal priority — the comment in enable_builtins warns 'the most specific converters should appear below the most generic converters' in source order.
- Test workflow: cd packages/markitdown && pip install hatch && hatch test (hatch env auto-installs [all] + openai); or run pytest directly after installing 'markitdown[all]' editable. CI also runs per-package pytest for markitdown-ocr and markitdown-mcp.
- New test-vector files go in tests/test_files plus a FileTestVector entry in tests/_test_vectors.py; expected-output assertions are substring-based (must_include/must_not_include), not snapshot diffs.
- Deprecated kwargs file_extension/url still flow through convert_local/convert_stream/convert_response and _convert injects them from stream_info — preserve them when refactoring signatures or tests like test_zip_kwargs.py will break.
- MCP error contract: do not let raw exceptions leak — convert_to_markdown deliberately maps each exception class to a sanitized ToolError; new exception types from the library need explicit handling there.
- Read first: _markitdown.py (engine), _base_converter.py + _stream_info.py (contracts), one representative converter (_docx_converter.py for the optional-dep + HTML-intermediate pattern), and _markdownify.py for output shaping; for plugin work, _plugin.py in both markitdown-ocr and markitdown-sample-plugin. (inferred)
- Known trap: tests/test_module_vectors.py sets skip_remote only on GITHUB_ACTIONS — local runs may hit raw.githubusercontent.com for some vectors, so sandboxed agents should set GITHUB_ACTIONS=1 or expect network tests.
- Version bumps happen per package in each src/<pkg>/__about__.py via hatch dynamic versioning — there is no root-level version file.