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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: td-task
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Fast TUI and CLI manager for tasks and Markdown notes
5
5
  Requires-Python: >=3.14
6
6
  Requires-Dist: cryptography>=42.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "td-task"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Fast TUI and CLI manager for tasks and Markdown notes"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -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 PyPI."""
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
- ["uv", "tool", "upgrade", "td-task"],
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