papertrail 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.3
2
+ Name: papertrail
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: ed cuss
6
+ Author-email: ed cuss <edcussmusic@gmail.com>
7
+ Requires-Dist: attrs>=25.4.0
8
+ Requires-Dist: black>=26.1.0
9
+ Requires-Dist: io-adapters==0.2.2
10
+ Requires-Dist: pytest>=9.0.2
11
+ Requires-Dist: python-dotenv>=1.2.1
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+
15
+ # papertrail
16
+
17
+ ### Motivation
18
+ - Documentation can get out of date quickly
19
+ - Sometimes its harder to explain what a function does in plain english than it is to just show what the input and outputs would be:
20
+ - e.g:
21
+ - "Multiply the value strings by the amount that is the key".
22
+ - vs
23
+ - `f({"a": 1, "b": 2, "c": 4}) == ['a', 'bb', 'cccc']`
24
+ - Tests are the best form of documentation because they complain when they go out of sync
25
+ - Doctests are an interesting way of writing tests however they can be fiddly and harder to set up than proper testing frameworks
26
+ - Resorting to only using doctests means missing out on features like fixtures and parameterisation
27
+ - Having some tests that are written in tests and some that are in doctests means keeping track of two places as the "source of truth"
28
+
29
+ ### Installation
30
+ ```shell
31
+ uv add papertrail
32
+ ```
33
+
34
+ ### Example
35
+ Use the `example` function to capture an example from a test to add to the docstring.
36
+
37
+ Wrap the example around the function before the equality operation and the function result will be captured and added to the docstring of the function.
38
+ This only happens if the tests pass!
39
+
40
+ ```python
41
+ from papertrail import example
42
+
43
+ def test_add():
44
+ assert example(add, 2, 3) == 5
45
+ ```
46
+
47
+ This has captured the example and now once the tests are finished the docstring of the function will be updated like this:
48
+
49
+ ```python
50
+ def add(a: int, b: int) -> int:
51
+ """Papertrail examples:
52
+
53
+ >>> add(2, 3) == 5
54
+ True
55
+ ::
56
+ """
57
+ return a + b
58
+ ```
59
+
60
+ You can use it with parametrized tests too:
61
+
62
+ ```python
63
+ @pytest.mark.parametrize(
64
+ ("args", "kwargs", "expected_result"),
65
+ [
66
+ pytest.param((2, 2), {}, 0),
67
+ pytest.param((2,), {"b": 3}, -1),
68
+ ],
69
+ )
70
+ def test_func_b(args, kwargs, expected_result):
71
+ assert example(func_b, *args, **kwargs) == expected_result
72
+ ```
73
+
74
+ This captures all the cases and adds them to the functions docstring:
75
+
76
+ ```python
77
+ def func_b(a: float, b: float) -> float:
78
+ """Simple docstring
79
+
80
+ Args:
81
+ a (float)
82
+ b (float)
83
+
84
+ Returns:
85
+ float
86
+
87
+ Papertrail examples:
88
+
89
+ >>> func_b(3, 4) == -1
90
+ True
91
+ ::
92
+ """
93
+ return a - b
94
+ ```
95
+
96
+ # Repo map
97
+ ```
98
+ ├── .github
99
+ │ └── workflows
100
+ │ ├── ci_tests.yaml
101
+ │ └── publish.yaml
102
+ ├── docs
103
+ │ └── source
104
+ │ └── conf.py
105
+ ├── src
106
+ │ └── papertrail
107
+ │ ├── adapters
108
+ │ │ ├── __init__.py
109
+ │ │ └── io_funcs.py # The IO functions for the IO adapters
110
+ │ ├── core
111
+ │ │ ├── collection
112
+ │ │ │ ├── __init__.py
113
+ │ │ │ ├── example.py # User entrypoint `example` and it's inner class `Example`
114
+ │ │ │ ├── record.py
115
+ │ │ │ └── recorder.py
116
+ │ │ ├── transformation
117
+ │ │ │ ├── __init__.py
118
+ │ │ │ ├── ast_editing.py
119
+ │ │ │ ├── format_examples.py
120
+ │ │ │ └── transform.py # Updates the the docstrings for all the funcs in the files in the example cache.
121
+ │ │ ├── __init__.py
122
+ │ │ └── logger.py
123
+ │ ├── __init__.py
124
+ │ └── __main__.py # the pytest hook for sessionfinish
125
+ ├── tests
126
+ │ ├── _mock_data
127
+ │ │ ├── mock_src
128
+ │ │ │ ├── __init__.py
129
+ │ │ │ ├── mod_a.py
130
+ │ │ │ └── mod_b.py
131
+ │ │ └── tests
132
+ │ │ ├── __init__.py
133
+ │ │ ├── conftest.py
134
+ │ │ ├── test_mod_a.py
135
+ │ │ └── test_mod_b.py
136
+ │ ├── core
137
+ │ │ ├── collection
138
+ │ │ │ ├── __init__.py
139
+ │ │ │ ├── test_example.py
140
+ │ │ │ ├── test_record.py
141
+ │ │ │ └── test_recorder.py
142
+ │ │ └── transformation
143
+ │ │ ├── __init__.py
144
+ │ │ ├── test_ast_editing.py
145
+ │ │ └── test_format_examples.py
146
+ │ └── test_main.py
147
+ ├── .pre-commit-config.yaml
148
+ ├── README.md
149
+ ├── pyproject.toml
150
+ ├── ruff.toml
151
+ └── uv.lock
152
+
153
+ (generated with repo-mapper-rs)
154
+ ::
155
+ ```
@@ -0,0 +1,141 @@
1
+ # papertrail
2
+
3
+ ### Motivation
4
+ - Documentation can get out of date quickly
5
+ - Sometimes its harder to explain what a function does in plain english than it is to just show what the input and outputs would be:
6
+ - e.g:
7
+ - "Multiply the value strings by the amount that is the key".
8
+ - vs
9
+ - `f({"a": 1, "b": 2, "c": 4}) == ['a', 'bb', 'cccc']`
10
+ - Tests are the best form of documentation because they complain when they go out of sync
11
+ - Doctests are an interesting way of writing tests however they can be fiddly and harder to set up than proper testing frameworks
12
+ - Resorting to only using doctests means missing out on features like fixtures and parameterisation
13
+ - Having some tests that are written in tests and some that are in doctests means keeping track of two places as the "source of truth"
14
+
15
+ ### Installation
16
+ ```shell
17
+ uv add papertrail
18
+ ```
19
+
20
+ ### Example
21
+ Use the `example` function to capture an example from a test to add to the docstring.
22
+
23
+ Wrap the example around the function before the equality operation and the function result will be captured and added to the docstring of the function.
24
+ This only happens if the tests pass!
25
+
26
+ ```python
27
+ from papertrail import example
28
+
29
+ def test_add():
30
+ assert example(add, 2, 3) == 5
31
+ ```
32
+
33
+ This has captured the example and now once the tests are finished the docstring of the function will be updated like this:
34
+
35
+ ```python
36
+ def add(a: int, b: int) -> int:
37
+ """Papertrail examples:
38
+
39
+ >>> add(2, 3) == 5
40
+ True
41
+ ::
42
+ """
43
+ return a + b
44
+ ```
45
+
46
+ You can use it with parametrized tests too:
47
+
48
+ ```python
49
+ @pytest.mark.parametrize(
50
+ ("args", "kwargs", "expected_result"),
51
+ [
52
+ pytest.param((2, 2), {}, 0),
53
+ pytest.param((2,), {"b": 3}, -1),
54
+ ],
55
+ )
56
+ def test_func_b(args, kwargs, expected_result):
57
+ assert example(func_b, *args, **kwargs) == expected_result
58
+ ```
59
+
60
+ This captures all the cases and adds them to the functions docstring:
61
+
62
+ ```python
63
+ def func_b(a: float, b: float) -> float:
64
+ """Simple docstring
65
+
66
+ Args:
67
+ a (float)
68
+ b (float)
69
+
70
+ Returns:
71
+ float
72
+
73
+ Papertrail examples:
74
+
75
+ >>> func_b(3, 4) == -1
76
+ True
77
+ ::
78
+ """
79
+ return a - b
80
+ ```
81
+
82
+ # Repo map
83
+ ```
84
+ ├── .github
85
+ │ └── workflows
86
+ │ ├── ci_tests.yaml
87
+ │ └── publish.yaml
88
+ ├── docs
89
+ │ └── source
90
+ │ └── conf.py
91
+ ├── src
92
+ │ └── papertrail
93
+ │ ├── adapters
94
+ │ │ ├── __init__.py
95
+ │ │ └── io_funcs.py # The IO functions for the IO adapters
96
+ │ ├── core
97
+ │ │ ├── collection
98
+ │ │ │ ├── __init__.py
99
+ │ │ │ ├── example.py # User entrypoint `example` and it's inner class `Example`
100
+ │ │ │ ├── record.py
101
+ │ │ │ └── recorder.py
102
+ │ │ ├── transformation
103
+ │ │ │ ├── __init__.py
104
+ │ │ │ ├── ast_editing.py
105
+ │ │ │ ├── format_examples.py
106
+ │ │ │ └── transform.py # Updates the the docstrings for all the funcs in the files in the example cache.
107
+ │ │ ├── __init__.py
108
+ │ │ └── logger.py
109
+ │ ├── __init__.py
110
+ │ └── __main__.py # the pytest hook for sessionfinish
111
+ ├── tests
112
+ │ ├── _mock_data
113
+ │ │ ├── mock_src
114
+ │ │ │ ├── __init__.py
115
+ │ │ │ ├── mod_a.py
116
+ │ │ │ └── mod_b.py
117
+ │ │ └── tests
118
+ │ │ ├── __init__.py
119
+ │ │ ├── conftest.py
120
+ │ │ ├── test_mod_a.py
121
+ │ │ └── test_mod_b.py
122
+ │ ├── core
123
+ │ │ ├── collection
124
+ │ │ │ ├── __init__.py
125
+ │ │ │ ├── test_example.py
126
+ │ │ │ ├── test_record.py
127
+ │ │ │ └── test_recorder.py
128
+ │ │ └── transformation
129
+ │ │ ├── __init__.py
130
+ │ │ ├── test_ast_editing.py
131
+ │ │ └── test_format_examples.py
132
+ │ └── test_main.py
133
+ ├── .pre-commit-config.yaml
134
+ ├── README.md
135
+ ├── pyproject.toml
136
+ ├── ruff.toml
137
+ └── uv.lock
138
+
139
+ (generated with repo-mapper-rs)
140
+ ::
141
+ ```
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "papertrail"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "ed cuss", email = "edcussmusic@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "attrs>=25.4.0",
12
+ "black>=26.1.0",
13
+ "io-adapters==0.2.2",
14
+ "pytest>=9.0.2",
15
+ "python-dotenv>=1.2.1",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.8.13,<0.9.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "hypothesis>=6.151.4",
25
+ "ipykernel>=7.1.0",
26
+ "pre-commit>=4.5.1",
27
+ "pytest>=9.0.2",
28
+ "pytest-cov>=7.0.0",
29
+ "repo-mapper-rs>=0.4.0",
30
+ "ruff>=0.14.14",
31
+ "sphinx>=9.0.4",
32
+ ]
33
+
34
+ # [project.entry-points.pytest11]
35
+ # papertrail = "papertrail.__main__"
36
+
37
+ [tool.coverage.run]
38
+ branch = true
39
+ source = ["src"]
40
+ omit = [
41
+ "src/papertrail/__main__.py",
42
+ "src/papertrail/core/logger.py"
43
+ ]
44
+
45
+ [tool.coverage.report]
46
+ show_missing = true
47
+ skip_covered = true
48
+ fail_under = 95
@@ -0,0 +1,3 @@
1
+ from papertrail.core import example
2
+
3
+ __all__ = ["example"]
@@ -0,0 +1,30 @@
1
+ """repo-map-desc: the pytest hook for sessionfinish
2
+
3
+ Calls an inner main function for testability
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Generator
9
+ from typing import Any
10
+
11
+ import pytest
12
+ from _pytest.config import ExitCode
13
+ from _pytest.main import Session
14
+
15
+ from papertrail.core.collection.recorder import _RECORDER, Recorder
16
+ from papertrail.core.transformation.transform import update_modified_docstrings
17
+
18
+
19
+ @pytest.hookimpl(hookwrapper=True, trylast=True)
20
+ def pytest_sessionfinish(
21
+ session: Session, # noqa: ARG001
22
+ exitstatus: int | ExitCode, # noqa: ARG001
23
+ ) -> Generator[Any, None, None]:
24
+ yield
25
+ main(_RECORDER)
26
+
27
+
28
+ def main(recorder: Recorder) -> None:
29
+ recorder.prepare_files().write_examples()
30
+ update_modified_docstrings(recorder.adapter, recorder.path)
File without changes
@@ -0,0 +1,43 @@
1
+ """repo-map-desc: The IO functions for the IO adapters
2
+
3
+ Currently only uses simple registries
4
+ """
5
+
6
+ import json
7
+ from enum import Enum, unique
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from io_adapters import register_read_fn, register_write_fn
12
+
13
+
14
+ @unique
15
+ class FileType(Enum):
16
+ STR = "str"
17
+ JSON = "json"
18
+
19
+
20
+ @register_read_fn(FileType.STR)
21
+ def read_str(path: str, **kwargs: dict[str, Any]) -> str:
22
+ return Path(path).read_text(**kwargs)
23
+
24
+
25
+ @register_write_fn(FileType.STR)
26
+ def write_str(data: str, path: str, **kwargs: dict[str, Any]) -> None:
27
+ _make_dirs(path).write_text(data, **kwargs)
28
+
29
+
30
+ @register_read_fn(FileType.JSON)
31
+ def read_json(path: str, **kwargs: dict[str, Any]) -> dict:
32
+ return json.loads(Path(path).read_text(), **kwargs)
33
+
34
+
35
+ @register_write_fn(FileType.JSON)
36
+ def write_json(data: str, path: str, **kwargs: dict[str, Any]) -> None:
37
+ _make_dirs(path).write_text(json.dumps(data, indent=2, sort_keys=False, default=str, **kwargs))
38
+
39
+
40
+ def _make_dirs(path: str) -> Path:
41
+ path = Path(path)
42
+ path.parent.mkdir(parents=True, exist_ok=True)
43
+ return path
@@ -0,0 +1,4 @@
1
+ from papertrail.core.collection import example
2
+ from papertrail.core.logger import REPO_ROOT, logger
3
+
4
+ __all__ = ["REPO_ROOT", "example", "logger"]
@@ -0,0 +1,5 @@
1
+ from papertrail.core.collection.example import Example, example
2
+ from papertrail.core.collection.record import ExampleRecord
3
+ from papertrail.core.collection.recorder import Recorder
4
+
5
+ __all__ = ["Example", "ExampleRecord", "Recorder", "example"]
@@ -0,0 +1,121 @@
1
+ """repo-map-desc: User entrypoint `example` and it's inner class `Example`
2
+
3
+ The equality operator is where the magic happens
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import inspect
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import Any, TypeVar
12
+
13
+ import attrs
14
+
15
+ from papertrail.core.collection.record import ExampleRecord
16
+ from papertrail.core.collection.recorder import _RECORDER, Recorder
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ @attrs.define
22
+ class Example:
23
+ fn: Callable
24
+ args: tuple[Any, ...]
25
+ kwargs: dict[str, Any]
26
+ value: Any
27
+ recorder: Recorder
28
+
29
+ def __eq__(self, expected: T) -> bool:
30
+ record = ExampleRecord(
31
+ fn_name=self.fn.__name__,
32
+ module=self.fn.__module__,
33
+ src_file=str(Path(inspect.getsourcefile(self.fn))),
34
+ args=self.args,
35
+ kwargs=self.kwargs,
36
+ returned=self.value,
37
+ expected=expected,
38
+ )
39
+
40
+ self.recorder.record_example(record)
41
+ return self.value == expected
42
+
43
+ def __hash__(self) -> int:
44
+ hash_value = "".join(
45
+ [
46
+ self.fn.__name__,
47
+ self.fn.__module__,
48
+ str(self.args),
49
+ str(self.kwargs),
50
+ str(self.value),
51
+ ]
52
+ )
53
+ return hash(hash_value)
54
+
55
+
56
+ def example(fn: Callable, *args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Example:
57
+ """Capture an example from a test to add to the docstring.
58
+
59
+ Wrap the example around the function before the equality operation and the function result will be captured and added to the docstring of the function.
60
+ This only happens if the tests pass!
61
+
62
+ .. code-block:: python
63
+
64
+ from papertrail import example
65
+
66
+ def test_add():
67
+ assert example(add, 2, 3) == 5
68
+
69
+
70
+ This has captured the example and now once the tests are finished the docstring of the function will be updated like this:
71
+
72
+ .. code-block:: python
73
+
74
+ def add(a: int, b: int) -> int:
75
+ \"\"\"Papertrail examples:
76
+
77
+ >>> add(2, 3) == 5
78
+ True
79
+ ::
80
+ \"\"\"
81
+ return a + b
82
+
83
+
84
+ You can use it with parametrized tests too:
85
+
86
+ .. code-block:: python
87
+
88
+ @pytest.mark.parametrize(
89
+ ("args", "kwargs", "expected_result"),
90
+ [
91
+ pytest.param((2, 2), {}, 0),
92
+ pytest.param((2,), {"b": 3}, -1),
93
+ ],
94
+ )
95
+ def test_func_b(args, kwargs, expected_result):
96
+ assert example(func_b, *args, **kwargs) == expected_result
97
+
98
+ This captures all the cases and adds them to the functions docstring:
99
+
100
+ .. code-block:: python
101
+
102
+ def func_b(a: float, b: float) -> float:
103
+ \"\"\"Simple docstring
104
+
105
+ Args:
106
+ a (float)
107
+ b (float)
108
+
109
+ Returns:
110
+ float
111
+
112
+ Papertrail examples:
113
+
114
+ >>> func_b(3, 4) == -1
115
+ True
116
+ ::
117
+ \"\"\"
118
+ return a - b
119
+ """
120
+ value = fn(*args, **kwargs)
121
+ return Example(fn, args, kwargs, value, recorder=_RECORDER)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import attrs
6
+
7
+
8
+ @attrs.define(frozen=True, eq=True)
9
+ class ExampleRecord:
10
+ fn_name: str
11
+ module: str
12
+ src_file: str
13
+ args: tuple[Any, ...]
14
+ kwargs: dict[str, Any]
15
+ returned: Any
16
+ expected: Any
17
+
18
+ def to_dict(self) -> dict[str, str]:
19
+ return attrs.asdict(self)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Self
5
+
6
+ import attrs
7
+ from io_adapters import IoAdapter, RealAdapter
8
+
9
+ from papertrail.adapters.io_funcs import FileType
10
+ from papertrail.core.collection.record import ExampleRecord
11
+
12
+
13
+ @attrs.define
14
+ class Recorder:
15
+ path: str | Path = "./.papertrail_cache/examples.json"
16
+ records: list[ExampleRecord] = attrs.field(factory=list)
17
+ adapter: IoAdapter = attrs.field(factory=RealAdapter)
18
+ files: dict = attrs.field(factory=dict)
19
+
20
+ def __attrs_post_init__(self) -> None:
21
+ self.path = Path(self.path)
22
+
23
+ def record_example(self, example: ExampleRecord) -> Self:
24
+ self.records.append(example)
25
+ return self
26
+
27
+ def prepare_files(self) -> Self:
28
+ self.files[(self.path, FileType.JSON)] = [r.to_dict() for r in self.records]
29
+ self.files[(self.path.parent / ".gitignore", FileType.STR)] = (
30
+ "# automatically created by papertrail\n*"
31
+ )
32
+ return self
33
+
34
+ def write_examples(self) -> Self:
35
+ for k, data in self.files.items():
36
+ path, file_type = k
37
+ self.adapter.write(data, path, file_type)
38
+ return self
39
+
40
+
41
+ _RECORDER = Recorder()
@@ -0,0 +1,31 @@
1
+ import logging
2
+ import os
3
+ from logging.handlers import RotatingFileHandler
4
+ from pathlib import Path
5
+
6
+ from dotenv import load_dotenv
7
+
8
+ logger = logging.getLogger(__name__)
9
+ logger.propagate = False
10
+
11
+ REPO_ROOT = Path(__file__).absolute().parents[3]
12
+ load_dotenv(f"{REPO_ROOT}/envs/.env")
13
+
14
+ if os.getenv("LOGGING_ENABLED", "false").lower() == "true":
15
+ formatter = logging.Formatter(
16
+ "%(asctime)s | %(levelname)-8s [%(filename)s:%(lineno)d:%(funcName)s] %(message)s"
17
+ )
18
+ logger.setLevel(logging.DEBUG)
19
+ stream_handler = logging.StreamHandler()
20
+ stream_handler.setLevel(logging.DEBUG)
21
+ stream_handler.setFormatter(formatter)
22
+
23
+ log_path = f"{REPO_ROOT}/logs/app.log"
24
+ os.makedirs(os.path.dirname(log_path), exist_ok=True)
25
+
26
+ file_handler = RotatingFileHandler(log_path, maxBytes=2_000_000, backupCount=1)
27
+ file_handler.setLevel(logging.DEBUG)
28
+ file_handler.setFormatter(formatter)
29
+
30
+ logger.addHandler(stream_handler)
31
+ logger.addHandler(file_handler)
@@ -0,0 +1,84 @@
1
+ import ast
2
+ import re
3
+ from typing import Self
4
+
5
+ import attrs
6
+
7
+
8
+ def update_function_docstrings(code: str, examples: dict[str, str]) -> str:
9
+ replacements = create_replacements(code, examples)
10
+ return replace_docstrings(code, replacements)
11
+
12
+
13
+ @attrs.define(frozen=True)
14
+ class DocString:
15
+ doc: str
16
+ start_line: int
17
+ end_line: int
18
+ indent: str = " "
19
+
20
+ @classmethod
21
+ def from_doc_node(cls, node: ast.AST) -> Self:
22
+ return cls(node.value.value.rstrip(), node.lineno - 1, node.end_lineno)
23
+
24
+ @classmethod
25
+ def from_non_doc_node(cls, node: ast.AST) -> Self:
26
+ start = node.lineno - 1
27
+ return cls("", start, start)
28
+
29
+ def with_example(self, example: str) -> Self:
30
+ example = "".join(
31
+ [
32
+ f"{self.indent}{line}" if line.strip() else line
33
+ for line in example.splitlines(keepends=True)
34
+ ]
35
+ )
36
+ pattern = r"(?ms)^[ \t]*Papertrail examples:\n.*?\n[ \t]*::"
37
+ replaced_doc, n = re.subn(pattern, example, self.doc)
38
+
39
+ new_doc = replaced_doc if n else f"{self.doc}\n\n{example}"
40
+ return DocString(new_doc.strip(), self.start_line, self.end_line)
41
+
42
+ def to_doc(self) -> str:
43
+ return f'{self.indent}"""{self.doc.strip()}\n{self.indent}"""\n'
44
+
45
+
46
+ def create_replacements(code: str, examples: dict[str, str]) -> list[DocString]:
47
+ tree = ast.parse(code)
48
+
49
+ replacements = []
50
+
51
+ for node in ast.walk(tree):
52
+ if not _is_fn_with_body(node) or not (example := examples.get(node.name, "")):
53
+ continue
54
+
55
+ first = node.body[0]
56
+
57
+ if _is_docstring(first):
58
+ docstring = DocString.from_doc_node(first)
59
+ else:
60
+ docstring = DocString.from_non_doc_node(first)
61
+
62
+ replacements.append(docstring.with_example(example))
63
+
64
+ return replacements
65
+
66
+
67
+ def replace_docstrings(code: str, replacements: list[DocString]) -> str:
68
+ lines = code.splitlines(keepends=True)
69
+ for doc in reversed(replacements):
70
+ lines[doc.start_line : doc.end_line] = [doc.to_doc()]
71
+
72
+ return "".join(lines)
73
+
74
+
75
+ def _is_fn_with_body(node: ast.AST) -> bool:
76
+ return isinstance(node, ast.FunctionDef) and node.body
77
+
78
+
79
+ def _is_docstring(node: ast.AST) -> bool:
80
+ return (
81
+ isinstance(node, ast.Expr)
82
+ and isinstance(node.value, ast.Constant)
83
+ and isinstance(node.value.value, str)
84
+ )
@@ -0,0 +1,50 @@
1
+ from collections import defaultdict
2
+
3
+ import black
4
+
5
+ from papertrail.core.collection.record import ExampleRecord
6
+
7
+
8
+ def collect_example_strs(examples: list[dict]) -> dict[str, dict[str, list[str]]]:
9
+ fn_examples = defaultdict(_inner)
10
+
11
+ for data in examples:
12
+ ex = ExampleRecord(**data)
13
+
14
+ if ex.returned != ex.expected:
15
+ continue
16
+
17
+ fn_examples[ex.src_file][ex.fn_name].append(example_to_str(ex))
18
+
19
+ return {k: dict(v) for k, v in fn_examples.items()}
20
+
21
+
22
+ def _inner() -> defaultdict:
23
+ return defaultdict(list)
24
+
25
+
26
+ def example_to_str(example: ExampleRecord) -> str:
27
+ sig = ", ".join(
28
+ part
29
+ for part in (
30
+ ", ".join(map(str, example.args)),
31
+ ", ".join(f"{k}={v}" for k, v in example.kwargs.items()),
32
+ )
33
+ if part
34
+ )
35
+ expr = f"{example.fn_name}({sig}) == {example.returned}"
36
+ formatted = black.format_str(expr, mode=black.Mode())
37
+ lines = formatted.rstrip().splitlines()
38
+ doctest = "\n".join(
39
+ f"{' >>>' if i == 0 else ' ...'} {line}" for i, line in enumerate(lines)
40
+ )
41
+ return f"{doctest}\n True"
42
+
43
+
44
+ def reduce_examples_to_example_str(
45
+ fn_examples: dict[str, dict[str, list[str]]],
46
+ ) -> dict[str, dict[str, str]]:
47
+ return {
48
+ path: {k: "Papertrail examples:\n\n" + "\n\n".join(v) + "\n::" for k, v in fn.items()}
49
+ for path, fn in fn_examples.items()
50
+ }
@@ -0,0 +1,24 @@
1
+ """repo-map-desc: Updates the the docstrings for all the funcs in the files in the example cache.
2
+
3
+ This is the main entry point for the transformation module.
4
+ """
5
+
6
+ from io_adapters import IoAdapter
7
+
8
+ from papertrail.adapters.io_funcs import FileType
9
+ from papertrail.core.transformation.ast_editing import update_function_docstrings
10
+ from papertrail.core.transformation.format_examples import (
11
+ collect_example_strs,
12
+ reduce_examples_to_example_str,
13
+ )
14
+
15
+
16
+ def update_modified_docstrings(adapter: IoAdapter, examples_cache_path: str) -> None:
17
+ examples = adapter.read(examples_cache_path, FileType.JSON)
18
+ reduced_examples = reduce_examples_to_example_str(collect_example_strs(examples))
19
+
20
+ for path, module_examples in reduced_examples.items():
21
+ code = adapter.read(path, FileType.STR)
22
+ new_code = update_function_docstrings(code, module_examples)
23
+ if new_code != code:
24
+ adapter.write(new_code, path, FileType.STR)