tofix 1.0.0__py3-none-any.whl

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.
tofix/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from tofix.core import Tofix, Format, Line
2
+ from tofix.cli import app
3
+
4
+ __all__ = ["Tofix", "Format", "Line", "app"]
tofix/cli.py ADDED
@@ -0,0 +1,29 @@
1
+ from argparse import ArgumentParser
2
+ from tofix import Tofix, Format
3
+ import os
4
+
5
+
6
+ def app():
7
+ parser = ArgumentParser()
8
+ parser.add_argument(
9
+ "--format",
10
+ choices=[Format.HUMAN, Format.MACHINE, Format.JSON],
11
+ default=Format.HUMAN,
12
+ )
13
+ parser.add_argument("--cached", action="store_true")
14
+ parser.add_argument("--unstaged", action="store_true")
15
+ args = parser.parse_args()
16
+
17
+ todos = Tofix(unstaged=args.unstaged, cached=args.cached)
18
+ _, lines = todos.files_and_lines()
19
+
20
+ if os.isatty(1) and args.format == Format.HUMAN:
21
+ Tofix.human_format(lines)
22
+ elif not os.isatty(1) or args.format == Format.MACHINE:
23
+ Tofix.machine_format(lines)
24
+ elif args.format == Format.JSON:
25
+ Tofix.json_format(lines)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ app()
tofix/core.py ADDED
@@ -0,0 +1,174 @@
1
+ """
2
+ Implements the `todos` application. See `todos.Tofix` for more information.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ import os
8
+ import json
9
+ from typing import List, Optional, Any, Tuple, Dict
10
+ from git import Git
11
+
12
+
13
+ def _remove_nones(values: List[Optional[Any]]) -> List[Any]:
14
+ """Remove all None values from a list."""
15
+ return [v for v in values if v is not None]
16
+
17
+
18
+ class Colors:
19
+ BOLD_GREEN = "\033[1;32m"
20
+ BOLD_YELLOW = "\033[1;33m"
21
+ RESET = "\033[0m"
22
+
23
+
24
+ class Format(str, Enum):
25
+ """Formatting options for stdout."""
26
+
27
+ HUMAN = "human"
28
+ MACHINE = "machine"
29
+ JSON = "json"
30
+
31
+
32
+ @dataclass
33
+ class Line:
34
+ """Dataclass for a line containing the "todo" key."""
35
+
36
+ path: str
37
+ number: int
38
+ text: str
39
+
40
+ def to_dict(self):
41
+ """Represent as a dict."""
42
+ return {"file": self.path, "line": self.number, "text": self.text}
43
+
44
+
45
+ class Tofix:
46
+ """Implements the `todos` application.
47
+
48
+ Exposes one method `files_and_lines` which returns a list of raw file paths and `Line`
49
+ objects for every line in the currently checked out branch that contains a specified
50
+ "todo" `key`.
51
+
52
+ Uses `gitpython` under the hood to run the necessary `git cherry` and `git diff`
53
+ commands.
54
+ """
55
+
56
+ base_branch: str
57
+ key: str
58
+ cached: bool
59
+ unstaged: bool
60
+ g: Git
61
+
62
+ def __init__(
63
+ self,
64
+ base_branch: str = "main",
65
+ key: str = "TODO",
66
+ cached: bool = False,
67
+ unstaged: bool = False,
68
+ ):
69
+ self.base_branch = base_branch
70
+ self.key = key
71
+ self.cached = cached
72
+ self.unstaged = unstaged
73
+ self.g = Git(os.getcwd())
74
+
75
+ if self.cached and self.unstaged:
76
+ raise ValueError("cached and unstaged are mutually exclusive options")
77
+
78
+ def _commits(self) -> List[str]:
79
+ # cherry outputs each commit that's in one branch but not the other
80
+ cherry_output = self.g.cherry(["-v", self.base_branch])
81
+ all_cherries = cherry_output.split("\n")
82
+
83
+ # git diff appends a "+ " in front of commits in the current branch that are not
84
+ # in the base branch"
85
+ added_cherries = [c for c in all_cherries if c[:2] == "+ "]
86
+
87
+ # there's a space between the "+" and the commit hash
88
+ hashes = [c.split(" ")[1] for c in added_cherries]
89
+ return hashes
90
+
91
+ def _files(self, left: str, right: Optional[str] = None) -> List[str]:
92
+ # Get names of files that changed in this commit range that have the key in them.
93
+ flags = ["--name-only", "-S", self.key]
94
+ if self.cached:
95
+ flags.append("--cached")
96
+ diff_args = _remove_nones([*flags, left, right])
97
+
98
+ diff_output = self.g.diff(diff_args)
99
+ files_with_keys = [line for line in diff_output.split("\n") if line != ""]
100
+ return files_with_keys
101
+
102
+ def _files_and_lines(
103
+ self, left: str, right: Optional[str]
104
+ ) -> Tuple[List[str], List[Line]]:
105
+ flags = ["-U999999"]
106
+ if self.cached:
107
+ flags.append("--cached")
108
+
109
+ paths = self._files(left, right)
110
+ result: List[Line] = []
111
+
112
+ for path in paths:
113
+ diff_args = _remove_nones([*flags, left, right, "--", path])
114
+ diff_output = self.g.diff(diff_args)
115
+
116
+ # ignore the first 6 lines, which display metadata
117
+ diff_lines = diff_output.split("\n")[6:]
118
+
119
+ # we need to figure out what the line number is for each added line
120
+ lines_with_key = [
121
+ Line(path=path, number=number, text=text[1:])
122
+ for number, text in enumerate(diff_lines)
123
+ if text[:1] == "+" and self.key in text
124
+ ]
125
+ result += lines_with_key
126
+
127
+ return paths, result
128
+
129
+ def files_and_lines(self) -> Tuple[List[str], List[Line]]:
130
+ """Return a list of `Line` objects for added line containing `self.key`."""
131
+ commits = self._commits()
132
+
133
+ # On the base branch there are no added commits, so nothing was introduced here.
134
+ if not commits:
135
+ return [], []
136
+
137
+ # We want to diff with the commit *before* the first one we added in our branch
138
+ left = f"{commits[0]}^"
139
+
140
+ # The right side of the diff should be empty unless we're only looking at
141
+ # committed files
142
+ right = None if self.cached or self.unstaged else commits[-1]
143
+
144
+ return self._files_and_lines(left, right)
145
+
146
+ @staticmethod
147
+ def _lines_by_path(lines: List[Line]) -> Dict[str, List[Line]]:
148
+ mapping = {}
149
+ for line in lines:
150
+ if mapping.get(line.path) is None:
151
+ mapping[line.path] = []
152
+ mapping[line.path].append(line)
153
+ return mapping
154
+
155
+ @classmethod
156
+ def human_format(cls, lines: List[Line]):
157
+ """Print the lines in a format comparable to that of `ack` and `ag`."""
158
+ for path, _lines in Tofix._lines_by_path(lines).items():
159
+ print(f"{Colors.BOLD_GREEN}{path}{Colors.RESET}")
160
+ for line in _lines:
161
+ print(f"{Colors.BOLD_YELLOW}{line.number}{Colors.RESET}: {line.text}")
162
+
163
+ @classmethod
164
+ def machine_format(cls, lines: List[Line]):
165
+ """Print the lines in a format consumable by `fpp`."""
166
+ for line in lines:
167
+ print(f"{line.path}:{line.number}:{line.text}")
168
+
169
+ @classmethod
170
+ def json_format(cls, lines: List[Line]):
171
+ """Print the lines as a json array to stdout."""
172
+ objects = [line.to_dict() for line in lines]
173
+ output = json.dumps(objects, indent=2)
174
+ print(output)
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: tofix
3
+ Version: 1.0.0
4
+ Summary: Find TODO and FIXME comments introduced on the current branch vs a base branch.
5
+ Keywords: fixme,git,task,todo
6
+ Author: Piper Maxine Baker
7
+ Author-email: Piper Maxine Baker <dev@piper.community>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.md
10
+ Requires-Dist: gitpython>=3.1
11
+ Maintainer: Piper Maxine Baker
12
+ Maintainer-email: Piper Maxine Baker <dev@piper.community>
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+
16
+ # `tofix`
17
+
18
+ A tool to find all `TODO` and `FIXME` comments introduced on your dev branch. No more
19
+ polluting your task list with those left by previous developers in the codebase. Just the
20
+ changes *you* made.
21
+
22
+ ## usage
23
+
24
+ Run `tofix` anywhere in a git repository while on a non-`main` branch. If outputting
25
+ to a terminal, it gives a nice, human-readable format.
26
+
27
+ ```
28
+ > tofix
29
+ README.md
30
+ 7:TODO write an intro
31
+ 8:TODO write an outro
32
+ todos.sh
33
+ 48:for file in $(git diff --name-only -S"TODO" "${earliest}^" $latest); do
34
+ 52: | grep "TODO" \
35
+ ```
36
+
37
+ If piped to another command (e.g., [`fpp`](https://github.com/facebook/PathPicker)), it
38
+ uses a machine-readable format:
39
+
40
+ ```
41
+ > tofix | fpp
42
+ README.md:7:TODO write an intro
43
+ README.md:8:TODO write an outro
44
+ todos.sh:48:for file in $(git diff --name-only -S"TODO" "${earliest}^" $latest); do
45
+ todos.sh:52: | grep "TODO" \
46
+
47
+ ________________________________________________________________________________________________________
48
+ [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick select mode, [c] command mode
49
+ ```
50
+
51
+ Both of these formats are inspired by the output of
52
+ [`ag`](https://github.com/mizuno-as/silversearcher-ag).
53
+
54
+ ## future work
55
+
56
+ - [ ] `emacs` integration
57
+ - [ ] `vscode` integration
@@ -0,0 +1,8 @@
1
+ tofix/__init__.py,sha256=7tkiTxRC0UNWR7Xe5Ny-plyrL-kVTiiC8FRaUeOPAm4,115
2
+ tofix/cli.py,sha256=GtI7Xfm-QCzRA-FSSU5xs_kZO68aV5hdGS3yXrwZkns,805
3
+ tofix/core.py,sha256=oPIU6Qbkebbfpd1Ps30n9K2kUXnYunl_2qinU62tx7k,5552
4
+ tofix-1.0.0.dist-info/licenses/LICENSE.md,sha256=YVFy62PThcARSS5uQ3BVIZ7xnysxTxIsEC6-iP1QvvI,1090
5
+ tofix-1.0.0.dist-info/WHEEL,sha256=jK0lbM7sVtq70msNoYotEXYS3OJMDdns2CRgyjhimnE,81
6
+ tofix-1.0.0.dist-info/entry_points.txt,sha256=XD2wLLxe2a6h_SkVT0salPuVraazvn2GC9I3ROvAXCY,41
7
+ tofix-1.0.0.dist-info/METADATA,sha256=xATQ40gQSGVJ8MfMW7xFF9cgijXWRKvyyF8hriUZ8Cc,1735
8
+ tofix-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.25
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ tofix = tofix.cli:app
3
+
@@ -0,0 +1,19 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright © 2026 Piper Maxine Baker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
6
+ software and associated documentation files (the "Software"), to deal in the Software
7
+ without restriction, including without limitation the rights to use, copy, modify, merge,
8
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
9
+ to whom the Software is furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or
12
+ substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19
+ DEALINGS IN THE SOFTWARE.