td-task 0.2.1__tar.gz → 0.2.2__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.
- {td_task-0.2.1 → td_task-0.2.2}/PKG-INFO +1 -1
- {td_task-0.2.1 → td_task-0.2.2}/pyproject.toml +1 -1
- {td_task-0.2.1 → td_task-0.2.2}/src/td/CHANGELOG.md +5 -0
- {td_task-0.2.1 → td_task-0.2.2}/src/td/__main__.py +62 -3
- td_task-0.2.2/tests/test_update.py +141 -0
- {td_task-0.2.1 → td_task-0.2.2}/.env.enc +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/.github/workflows/publish.yml +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/.gitignore +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/.python-version +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/README.md +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/assets/demo.gif +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/demo.tape +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/src/td/__init__.py +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/src/td/db.py +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/src/td/terminal.py +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/src/td/tui.py +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/tests/test_cli_notes.py +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/tests/test_notes.py +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/tests/test_tui_notes.py +0 -0
- {td_task-0.2.1 → td_task-0.2.2}/uv.lock +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2] - 2026-08-05
|
|
4
|
+
- Fix `td update` silently reporting "already up-to-date" on installs made from a local checkout.
|
|
5
|
+
- Pull the source checkout with `git pull --ff-only` and force a rebuild before upgrading such installs.
|
|
6
|
+
- Abort the update instead of rebuilding stale code when the pull fails.
|
|
7
|
+
|
|
3
8
|
## [0.2.1] - 2026-08-04
|
|
4
9
|
- Soft-wrap long note lines to terminal width without changing stored Markdown.
|
|
5
10
|
- Keep Markdown preview styling across wrapped rows and scroll with the active cursor.
|
|
@@ -393,13 +393,72 @@ def _run_list(list_name: str) -> None:
|
|
|
393
393
|
console.print(Text("─" * width, style="dim"))
|
|
394
394
|
|
|
395
395
|
|
|
396
|
+
def _source_checkout() -> str | None:
|
|
397
|
+
"""Return the git checkout td was installed from, or None for a PyPI install."""
|
|
398
|
+
import subprocess
|
|
399
|
+
import tomllib
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
result = subprocess.run(
|
|
403
|
+
["uv", "tool", "dir"], capture_output=True, text=True, timeout=10,
|
|
404
|
+
)
|
|
405
|
+
if result.returncode != 0:
|
|
406
|
+
return None
|
|
407
|
+
receipt = os.path.join(result.stdout.strip(), "td-task", "uv-receipt.toml")
|
|
408
|
+
with open(receipt, "rb") as f:
|
|
409
|
+
data = tomllib.load(f)
|
|
410
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
for requirement in data.get("tool", {}).get("requirements", []):
|
|
414
|
+
directory = requirement.get("directory")
|
|
415
|
+
if directory and os.path.isdir(os.path.join(directory, ".git")):
|
|
416
|
+
return directory
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _source_version(source_dir: str) -> str | None:
|
|
421
|
+
"""Read the version declared by the source checkout's pyproject.toml."""
|
|
422
|
+
import tomllib
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
with open(os.path.join(source_dir, "pyproject.toml"), "rb") as f:
|
|
426
|
+
return tomllib.load(f).get("project", {}).get("version")
|
|
427
|
+
except (OSError, ValueError):
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
|
|
396
431
|
def _run_update() -> None:
|
|
397
|
-
"""Update td to the latest version from
|
|
432
|
+
"""Update td to the latest version, from the local checkout when installed from one."""
|
|
398
433
|
import subprocess
|
|
434
|
+
from importlib.metadata import version as _pkg_version
|
|
435
|
+
|
|
436
|
+
upgrade_cmd = ["uv", "tool", "upgrade", "td-task"]
|
|
437
|
+
source_dir = _source_checkout()
|
|
438
|
+
|
|
439
|
+
if source_dir:
|
|
440
|
+
print(f"Pulling latest source from {source_dir}...")
|
|
441
|
+
pull = subprocess.run(
|
|
442
|
+
["git", "-C", source_dir, "pull", "--ff-only"],
|
|
443
|
+
capture_output=True, text=True, timeout=60,
|
|
444
|
+
)
|
|
445
|
+
if pull.returncode != 0:
|
|
446
|
+
print(f"✗ git pull failed: {pull.stderr.strip()}")
|
|
447
|
+
sys.exit(1)
|
|
448
|
+
try:
|
|
449
|
+
installed = _pkg_version("td-task")
|
|
450
|
+
except Exception:
|
|
451
|
+
installed = None
|
|
452
|
+
if installed and _source_version(source_dir) == installed:
|
|
453
|
+
print("already up-to-date.")
|
|
454
|
+
return
|
|
455
|
+
# A path install keeps resolving to the same requirement, so uv reports
|
|
456
|
+
# nothing to upgrade unless the rebuild is forced.
|
|
457
|
+
upgrade_cmd.append("--reinstall")
|
|
458
|
+
|
|
399
459
|
print("Updating td...")
|
|
400
460
|
result = subprocess.run(
|
|
401
|
-
|
|
402
|
-
capture_output=True, text=True, timeout=60,
|
|
461
|
+
upgrade_cmd, capture_output=True, text=True, timeout=300,
|
|
403
462
|
)
|
|
404
463
|
if result.returncode == 0:
|
|
405
464
|
output = (result.stdout + result.stderr).strip()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from contextlib import redirect_stdout
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
from td.__main__ import _run_update, _source_checkout, _source_version
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _completed(args, returncode=0, stdout="", stderr=""):
|
|
15
|
+
return subprocess.CompletedProcess(args, returncode, stdout, stderr)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SourceCheckoutTests(unittest.TestCase):
|
|
19
|
+
def setUp(self) -> None:
|
|
20
|
+
self.temp_dir = tempfile.TemporaryDirectory()
|
|
21
|
+
self.root = Path(self.temp_dir.name)
|
|
22
|
+
self.checkout = self.root / "src-checkout"
|
|
23
|
+
(self.checkout / ".git").mkdir(parents=True)
|
|
24
|
+
self.tool_dir = self.root / "tools"
|
|
25
|
+
(self.tool_dir / "td-task").mkdir(parents=True)
|
|
26
|
+
|
|
27
|
+
def tearDown(self) -> None:
|
|
28
|
+
self.temp_dir.cleanup()
|
|
29
|
+
|
|
30
|
+
def _write_receipt(self, body: str) -> None:
|
|
31
|
+
(self.tool_dir / "td-task" / "uv-receipt.toml").write_text(body, encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
def _tool_dir_call(self, *args, **kwargs):
|
|
34
|
+
return _completed(args, stdout=f"{self.tool_dir}\n")
|
|
35
|
+
|
|
36
|
+
def test_detects_directory_install(self) -> None:
|
|
37
|
+
self._write_receipt(
|
|
38
|
+
f'[tool]\nrequirements = [{{ name = "td-task", directory = "{self.checkout}" }}]\n'
|
|
39
|
+
)
|
|
40
|
+
with patch("subprocess.run", side_effect=self._tool_dir_call):
|
|
41
|
+
self.assertEqual(_source_checkout(), str(self.checkout))
|
|
42
|
+
|
|
43
|
+
def test_pypi_install_has_no_checkout(self) -> None:
|
|
44
|
+
self._write_receipt('[tool]\nrequirements = [{ name = "td-task" }]\n')
|
|
45
|
+
with patch("subprocess.run", side_effect=self._tool_dir_call):
|
|
46
|
+
self.assertIsNone(_source_checkout())
|
|
47
|
+
|
|
48
|
+
def test_directory_without_git_is_ignored(self) -> None:
|
|
49
|
+
plain = self.root / "not-a-repo"
|
|
50
|
+
plain.mkdir()
|
|
51
|
+
self._write_receipt(
|
|
52
|
+
f'[tool]\nrequirements = [{{ name = "td-task", directory = "{plain}" }}]\n'
|
|
53
|
+
)
|
|
54
|
+
with patch("subprocess.run", side_effect=self._tool_dir_call):
|
|
55
|
+
self.assertIsNone(_source_checkout())
|
|
56
|
+
|
|
57
|
+
def test_missing_receipt_returns_none(self) -> None:
|
|
58
|
+
with patch("subprocess.run", side_effect=self._tool_dir_call):
|
|
59
|
+
self.assertIsNone(_source_checkout())
|
|
60
|
+
|
|
61
|
+
def test_source_version_read_from_pyproject(self) -> None:
|
|
62
|
+
(self.checkout / "pyproject.toml").write_text(
|
|
63
|
+
'[project]\nname = "td-task"\nversion = "9.9.9"\n', encoding="utf-8"
|
|
64
|
+
)
|
|
65
|
+
self.assertEqual(_source_version(str(self.checkout)), "9.9.9")
|
|
66
|
+
|
|
67
|
+
def test_source_version_missing_pyproject(self) -> None:
|
|
68
|
+
self.assertIsNone(_source_version(str(self.checkout)))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RunUpdateTests(unittest.TestCase):
|
|
72
|
+
def test_source_install_pulls_before_rebuilding(self) -> None:
|
|
73
|
+
calls = []
|
|
74
|
+
|
|
75
|
+
def fake_run(args, **kwargs):
|
|
76
|
+
calls.append(args)
|
|
77
|
+
return _completed(args)
|
|
78
|
+
|
|
79
|
+
with patch("td.__main__._source_checkout", return_value="/repo"), \
|
|
80
|
+
patch("td.__main__._source_version", return_value="0.3.0"), \
|
|
81
|
+
patch("td.__main__._print_changelog"), \
|
|
82
|
+
patch("importlib.metadata.version", return_value="0.2.9"), \
|
|
83
|
+
patch("subprocess.run", side_effect=fake_run), \
|
|
84
|
+
redirect_stdout(io.StringIO()):
|
|
85
|
+
_run_update()
|
|
86
|
+
|
|
87
|
+
self.assertEqual(calls[0], ["git", "-C", "/repo", "pull", "--ff-only"])
|
|
88
|
+
self.assertEqual(calls[1], ["uv", "tool", "upgrade", "td-task", "--reinstall"])
|
|
89
|
+
|
|
90
|
+
def test_source_install_skips_rebuild_when_current(self) -> None:
|
|
91
|
+
calls = []
|
|
92
|
+
|
|
93
|
+
def fake_run(args, **kwargs):
|
|
94
|
+
calls.append(args)
|
|
95
|
+
return _completed(args)
|
|
96
|
+
|
|
97
|
+
buffer = io.StringIO()
|
|
98
|
+
with patch("td.__main__._source_checkout", return_value="/repo"), \
|
|
99
|
+
patch("td.__main__._source_version", return_value="0.2.9"), \
|
|
100
|
+
patch("importlib.metadata.version", return_value="0.2.9"), \
|
|
101
|
+
patch("subprocess.run", side_effect=fake_run), \
|
|
102
|
+
redirect_stdout(buffer):
|
|
103
|
+
_run_update()
|
|
104
|
+
|
|
105
|
+
self.assertEqual(calls, [["git", "-C", "/repo", "pull", "--ff-only"]])
|
|
106
|
+
self.assertIn("already up-to-date", buffer.getvalue())
|
|
107
|
+
|
|
108
|
+
def test_failed_pull_aborts_without_rebuilding(self) -> None:
|
|
109
|
+
calls = []
|
|
110
|
+
|
|
111
|
+
def fake_run(args, **kwargs):
|
|
112
|
+
calls.append(args)
|
|
113
|
+
return _completed(args, returncode=1, stderr="divergent branches")
|
|
114
|
+
|
|
115
|
+
buffer = io.StringIO()
|
|
116
|
+
with patch("td.__main__._source_checkout", return_value="/repo"), \
|
|
117
|
+
patch("subprocess.run", side_effect=fake_run), \
|
|
118
|
+
redirect_stdout(buffer):
|
|
119
|
+
with self.assertRaises(SystemExit):
|
|
120
|
+
_run_update()
|
|
121
|
+
|
|
122
|
+
self.assertEqual(calls, [["git", "-C", "/repo", "pull", "--ff-only"]])
|
|
123
|
+
self.assertIn("divergent branches", buffer.getvalue())
|
|
124
|
+
|
|
125
|
+
def test_pypi_install_upgrades_without_pull(self) -> None:
|
|
126
|
+
calls = []
|
|
127
|
+
|
|
128
|
+
def fake_run(args, **kwargs):
|
|
129
|
+
calls.append(args)
|
|
130
|
+
return _completed(args, stdout="Nothing to upgrade")
|
|
131
|
+
|
|
132
|
+
with patch("td.__main__._source_checkout", return_value=None), \
|
|
133
|
+
patch("subprocess.run", side_effect=fake_run), \
|
|
134
|
+
redirect_stdout(io.StringIO()):
|
|
135
|
+
_run_update()
|
|
136
|
+
|
|
137
|
+
self.assertEqual(calls, [["uv", "tool", "upgrade", "td-task"]])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|