stubpy.stub_merge

stubpy.stub_merge

Incremental stub update — merge newly generated stubs with an existing .pyi file while preserving manually edited content.

The merge system wraps auto-generated sections with marker comments:

# stubpy: auto-generated begin
... (generated content) ...
# stubpy: auto-generated end

On subsequent runs only the content between the markers is replaced. Content outside the markers (manual additions, hand-tuned overloads, etc.) is preserved unchanged.

Marker rules

  • Case-insensitive and whitespace-lenient: # Stubpy : Auto-Generated Begin and #stubpy:auto-generated begin are both valid.

  • Multiple pairs in one file are supported. Each begin/end pair is replaced independently.

  • Half-open markers are handled gracefully: - An end without a preceding begin → the start of the file is

    treated as the implicit begin (the generated header replaced).

    • A begin without a following end → the end of the file is treated as the implicit end (everything after the begin replaced).

  • Content outside all marker pairs is left untouched.

The markers are added to new stubs automatically by wrap_generated(). When merging into an existing file that has no markers at all, the entire existing file content is left unchanged and the new content is appended inside a fresh marker pair — giving the developer a chance to review the difference before committing.

Public API

wrap_generated()

Wrap a newly generated stub body with begin/end markers.

merge_stubs()

Merge a freshly generated stub into an existing .pyi file.

BEGIN_MARKER = '# stubpy: auto-generated begin'

Canonical form written into new stubs.

wrap_generated(content: str) str[source]

Wrap content between canonical begin/end markers.

The markers are added exactly once — they are not added again if content already contains them.

Parameters:

content (str) – A complete stub file string, typically the output of generate_stub().

Returns:

strcontent with # stubpy: auto-generated begin prepended and # stubpy: auto-generated end appended (with trailing newline).

Examples

>>> wrapped = wrap_generated("x: int\n")
>>> wrapped.startswith("# stubpy: auto-generated begin")
True
>>> wrapped.strip().endswith("# stubpy: auto-generated end")
True
merge_stubs(existing: str, generated: str) str[source]

Merge generated stub content into existing .pyi content.

Only the regions inside marker pairs are replaced. Everything outside the markers is left untouched.

Strategy when no markers exist in existing

The entire existing file is left as-is and the new content is appended as a new marked section. This is intentionally conservative — on the first run after adding markers the developer reviews the diff.

param existing:

Current content of the .pyi file on disk.

type existing:

str

param generated:

Freshly generated stub content (already passed through wrap_generated()).

type generated:

str

returns:

str – Merged content suitable for writing back to disk.

Examples

>>> existing = (
...     "# manual stuff\n"
...     "# stubpy: auto-generated begin\n"
...     "x: int\n"
...     "# stubpy: auto-generated end\n"
...     "# more manual\n"
... )
>>> generated = "# stubpy: auto-generated begin\ny: str\n# stubpy: auto-generated end\n"
>>> result = merge_stubs(existing, generated)
>>> "y: str" in result
True
>>> "# manual stuff" in result
True
>>> "# more manual" in result
True
>>> "x: int" not in result
True
read_and_merge(output_path: Path, generated: str) str[source]

Read output_path (if it exists) and merge generated into it.

Parameters:
  • output_path (Path) – Path to the existing .pyi file. When the file does not exist, generated (wrapped in markers) is returned directly.

  • generated (str) – Freshly generated stub content from generate_stub().

Returns:

str – Final merged content ready to be written to output_path.