chartremotely 0.3.0__tar.gz → 0.3.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.
- {chartremotely-0.3.0 → chartremotely-0.3.2}/PKG-INFO +24 -15
- {chartremotely-0.3.0 → chartremotely-0.3.2}/README.md +23 -14
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/assets/shortcut.plist +2 -2
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/mcpclient.py +4 -1
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/shortcut.py +1 -1
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely.egg-info/PKG-INFO +24 -15
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely.egg-info/SOURCES.txt +1 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/pyproject.toml +1 -1
- chartremotely-0.3.2/tests/test_changelog.py +85 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/LICENSE +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/__init__.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/answer.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/ax.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/cli.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/config.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/displays.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/doctor.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/guilock.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/hearing.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/keystore.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/layout.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/patron.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/popups.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/push.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/recent.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/registry.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/relay.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/requestlog.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/resolve.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/scales.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/screen.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/server.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/services.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/setup.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/snapshot.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/studies.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/symbol.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/tailnet.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/terminal.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/timeframe.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/understand.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/vocab.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/voice.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely/window.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely.egg-info/dependency_links.txt +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely.egg-info/entry_points.txt +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely.egg-info/requires.txt +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/chartremotely.egg-info/top_level.txt +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/setup.cfg +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_hearing.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_patron.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_popups.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_push.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_recent.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_relay.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_requestlog.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_resolve.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_scales.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_screen.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_setup.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_snapshot.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_stamp_changelog.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_timeframe.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_understand.py +0 -0
- {chartremotely-0.3.0 → chartremotely-0.3.2}/tests/test_voice.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chartremotely
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Remote control of desktop charting applications on macOS
|
|
5
5
|
Author: Lonnie VanZandt
|
|
6
6
|
License:
|
|
@@ -305,6 +305,7 @@ chartremotely serve run the local listener
|
|
|
305
305
|
chartremotely pair adopt this display to an operator
|
|
306
306
|
chartremotely relay hold a connection open for the operator
|
|
307
307
|
chartremotely doctor check everything the agent needs
|
|
308
|
+
chartremotely token print the local listener's access token
|
|
308
309
|
```
|
|
309
310
|
|
|
310
311
|
`serve` and `relay` are two different transports and most setups want both.
|
|
@@ -421,19 +422,23 @@ curl -fsSL https://chartremotely.tollbooth-dpyc.com/install.sh | sh
|
|
|
421
422
|
|
|
422
423
|
That installs [uv](https://docs.astral.sh/uv/) if it is missing, installs this
|
|
423
424
|
agent from PyPI (`uv tool install --python 3.12 'chartremotely[macos,setup]'`),
|
|
424
|
-
and runs `chartremotely setup
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
425
|
+
and runs `chartremotely setup`. Setup first says what is installing — version,
|
|
426
|
+
where it came from, the Python it runs on — lists its six steps, and asks
|
|
427
|
+
before changing anything:
|
|
428
|
+
|
|
429
|
+
1. **Identity** — proves who owns the display (your existing npub by answering
|
|
430
|
+
a Nostr DM, a key already saved on this Mac, or a new key it makes and saves
|
|
431
|
+
for you in the Keychain and, through Safari, in iCloud Passwords), then
|
|
432
|
+
pairs this Mac with that identity, with no code to copy;
|
|
433
|
+
2. **Tailscale** — gives the Mac its tailnet address with `tailscale serve`;
|
|
434
|
+
3. **Services** — installs the listener and the relay as launchd agents;
|
|
435
|
+
4. **Permissions** — asks macOS for Accessibility and Screen Recording for the
|
|
436
|
+
Python that runs them;
|
|
437
|
+
5. **Hearing** — fetches the Whisper speech model (about 1.6 GB, once) and
|
|
438
|
+
your display names;
|
|
439
|
+
6. **Shortcut** — makes this Mac's Shortcut, "ChartRemotely" (talk, or type
|
|
440
|
+
when it asks), opens it for import, and removes a "ChartRemotely Ask" an
|
|
441
|
+
older setup left in your library.
|
|
437
442
|
|
|
438
443
|
Run it again at any time: finished steps are skipped. `chartremotely doctor`
|
|
439
444
|
reports anything still missing.
|
|
@@ -473,7 +478,11 @@ switch to a grid - discovery follows, and takes about a second.
|
|
|
473
478
|
|
|
474
479
|
## Status
|
|
475
480
|
|
|
476
|
-
|
|
481
|
+
In service. Releases are published to PyPI and installed with the one-line
|
|
482
|
+
installer above; a paired Mac takes commands by voice, from the web app at
|
|
483
|
+
<https://chartremotely.tollbooth-dpyc.com>, and from any MCP client through the
|
|
484
|
+
operator ([ChartRemotely-mcp](https://github.com/lonniev/ChartRemotely-mcp)).
|
|
485
|
+
thinkorswim is the one charting application it drives today.
|
|
477
486
|
|
|
478
487
|
## License
|
|
479
488
|
|
|
@@ -78,6 +78,7 @@ chartremotely serve run the local listener
|
|
|
78
78
|
chartremotely pair adopt this display to an operator
|
|
79
79
|
chartremotely relay hold a connection open for the operator
|
|
80
80
|
chartremotely doctor check everything the agent needs
|
|
81
|
+
chartremotely token print the local listener's access token
|
|
81
82
|
```
|
|
82
83
|
|
|
83
84
|
`serve` and `relay` are two different transports and most setups want both.
|
|
@@ -194,19 +195,23 @@ curl -fsSL https://chartremotely.tollbooth-dpyc.com/install.sh | sh
|
|
|
194
195
|
|
|
195
196
|
That installs [uv](https://docs.astral.sh/uv/) if it is missing, installs this
|
|
196
197
|
agent from PyPI (`uv tool install --python 3.12 'chartremotely[macos,setup]'`),
|
|
197
|
-
and runs `chartremotely setup
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
198
|
+
and runs `chartremotely setup`. Setup first says what is installing — version,
|
|
199
|
+
where it came from, the Python it runs on — lists its six steps, and asks
|
|
200
|
+
before changing anything:
|
|
201
|
+
|
|
202
|
+
1. **Identity** — proves who owns the display (your existing npub by answering
|
|
203
|
+
a Nostr DM, a key already saved on this Mac, or a new key it makes and saves
|
|
204
|
+
for you in the Keychain and, through Safari, in iCloud Passwords), then
|
|
205
|
+
pairs this Mac with that identity, with no code to copy;
|
|
206
|
+
2. **Tailscale** — gives the Mac its tailnet address with `tailscale serve`;
|
|
207
|
+
3. **Services** — installs the listener and the relay as launchd agents;
|
|
208
|
+
4. **Permissions** — asks macOS for Accessibility and Screen Recording for the
|
|
209
|
+
Python that runs them;
|
|
210
|
+
5. **Hearing** — fetches the Whisper speech model (about 1.6 GB, once) and
|
|
211
|
+
your display names;
|
|
212
|
+
6. **Shortcut** — makes this Mac's Shortcut, "ChartRemotely" (talk, or type
|
|
213
|
+
when it asks), opens it for import, and removes a "ChartRemotely Ask" an
|
|
214
|
+
older setup left in your library.
|
|
210
215
|
|
|
211
216
|
Run it again at any time: finished steps are skipped. `chartremotely doctor`
|
|
212
217
|
reports anything still missing.
|
|
@@ -246,7 +251,11 @@ switch to a grid - discovery follows, and takes about a second.
|
|
|
246
251
|
|
|
247
252
|
## Status
|
|
248
253
|
|
|
249
|
-
|
|
254
|
+
In service. Releases are published to PyPI and installed with the one-line
|
|
255
|
+
installer above; a paired Mac takes commands by voice, from the web app at
|
|
256
|
+
<https://chartremotely.tollbooth-dpyc.com>, and from any MCP client through the
|
|
257
|
+
operator ([ChartRemotely-mcp](https://github.com/lonniev/ChartRemotely-mcp)).
|
|
258
|
+
thinkorswim is the one charting application it drives today.
|
|
250
259
|
|
|
251
260
|
## License
|
|
252
261
|
|
|
@@ -670,7 +670,7 @@
|
|
|
670
670
|
<key>WFWorkflowActionParameters</key>
|
|
671
671
|
<dict>
|
|
672
672
|
<key>WFSpeakTextRate</key>
|
|
673
|
-
<real>0.
|
|
673
|
+
<real>0.55</real>
|
|
674
674
|
<key>WFSpeakTextWait</key>
|
|
675
675
|
<true/>
|
|
676
676
|
<key>WFText</key>
|
|
@@ -754,7 +754,7 @@
|
|
|
754
754
|
<key>WFWorkflowActionParameters</key>
|
|
755
755
|
<dict>
|
|
756
756
|
<key>WFSpeakTextRate</key>
|
|
757
|
-
<real>0.
|
|
757
|
+
<real>0.55</real>
|
|
758
758
|
<key>WFSpeakTextWait</key>
|
|
759
759
|
<true/>
|
|
760
760
|
<key>WFText</key>
|
|
@@ -60,8 +60,11 @@ class Client:
|
|
|
60
60
|
self._ids = itertools.count(1)
|
|
61
61
|
|
|
62
62
|
def _send(self, message: dict) -> dict:
|
|
63
|
+
# The operator refuses a request with no bearer. The agent proves
|
|
64
|
+
# itself in its arguments, so at the connection it is anonymous.
|
|
63
65
|
headers = {"Content-Type": "application/json",
|
|
64
|
-
"Accept": "application/json, text/event-stream"
|
|
66
|
+
"Accept": "application/json, text/event-stream",
|
|
67
|
+
"Authorization": "Bearer anonymous"}
|
|
65
68
|
if self.session:
|
|
66
69
|
headers["mcp-session-id"] = self.session
|
|
67
70
|
request = urllib.request.Request(
|
|
@@ -34,7 +34,7 @@ NAME = "ChartRemotely"
|
|
|
34
34
|
RETIRED = "ChartRemotely Ask"
|
|
35
35
|
#: Speak Text's rate (0 to 1; Siri's own is 0.5). Replies are short and
|
|
36
36
|
#: expected, so a person follows them brisker than Siri's default.
|
|
37
|
-
SPEAK_RATE = 0.
|
|
37
|
+
SPEAK_RATE = 0.55
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def template() -> bytes:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chartremotely
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Remote control of desktop charting applications on macOS
|
|
5
5
|
Author: Lonnie VanZandt
|
|
6
6
|
License:
|
|
@@ -305,6 +305,7 @@ chartremotely serve run the local listener
|
|
|
305
305
|
chartremotely pair adopt this display to an operator
|
|
306
306
|
chartremotely relay hold a connection open for the operator
|
|
307
307
|
chartremotely doctor check everything the agent needs
|
|
308
|
+
chartremotely token print the local listener's access token
|
|
308
309
|
```
|
|
309
310
|
|
|
310
311
|
`serve` and `relay` are two different transports and most setups want both.
|
|
@@ -421,19 +422,23 @@ curl -fsSL https://chartremotely.tollbooth-dpyc.com/install.sh | sh
|
|
|
421
422
|
|
|
422
423
|
That installs [uv](https://docs.astral.sh/uv/) if it is missing, installs this
|
|
423
424
|
agent from PyPI (`uv tool install --python 3.12 'chartremotely[macos,setup]'`),
|
|
424
|
-
and runs `chartremotely setup
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
425
|
+
and runs `chartremotely setup`. Setup first says what is installing — version,
|
|
426
|
+
where it came from, the Python it runs on — lists its six steps, and asks
|
|
427
|
+
before changing anything:
|
|
428
|
+
|
|
429
|
+
1. **Identity** — proves who owns the display (your existing npub by answering
|
|
430
|
+
a Nostr DM, a key already saved on this Mac, or a new key it makes and saves
|
|
431
|
+
for you in the Keychain and, through Safari, in iCloud Passwords), then
|
|
432
|
+
pairs this Mac with that identity, with no code to copy;
|
|
433
|
+
2. **Tailscale** — gives the Mac its tailnet address with `tailscale serve`;
|
|
434
|
+
3. **Services** — installs the listener and the relay as launchd agents;
|
|
435
|
+
4. **Permissions** — asks macOS for Accessibility and Screen Recording for the
|
|
436
|
+
Python that runs them;
|
|
437
|
+
5. **Hearing** — fetches the Whisper speech model (about 1.6 GB, once) and
|
|
438
|
+
your display names;
|
|
439
|
+
6. **Shortcut** — makes this Mac's Shortcut, "ChartRemotely" (talk, or type
|
|
440
|
+
when it asks), opens it for import, and removes a "ChartRemotely Ask" an
|
|
441
|
+
older setup left in your library.
|
|
437
442
|
|
|
438
443
|
Run it again at any time: finished steps are skipped. `chartremotely doctor`
|
|
439
444
|
reports anything still missing.
|
|
@@ -473,7 +478,11 @@ switch to a grid - discovery follows, and takes about a second.
|
|
|
473
478
|
|
|
474
479
|
## Status
|
|
475
480
|
|
|
476
|
-
|
|
481
|
+
In service. Releases are published to PyPI and installed with the one-line
|
|
482
|
+
installer above; a paired Mac takes commands by voice, from the web app at
|
|
483
|
+
<https://chartremotely.tollbooth-dpyc.com>, and from any MCP client through the
|
|
484
|
+
operator ([ChartRemotely-mcp](https://github.com/lonniev/ChartRemotely-mcp)).
|
|
485
|
+
thinkorswim is the one charting application it drives today.
|
|
477
486
|
|
|
478
487
|
## License
|
|
479
488
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The changelog must not be the reason two independent changes conflict.
|
|
2
|
+
|
|
3
|
+
`CHANGELOG.md` was the single most conflict-prone file in the fleet, and not
|
|
4
|
+
through carelessness: every pull request appended to the same
|
|
5
|
+
`### Added` / `### Fixed` anchors of the same `## [Unreleased]` section, so any
|
|
6
|
+
two concurrent PRs collided there even when their code touched nothing in
|
|
7
|
+
common. Six did in `beesknees-mcp` in one day.
|
|
8
|
+
|
|
9
|
+
If A and B are independent, it must not matter whether they land A-then-B or
|
|
10
|
+
B-then-A. These tests hold that as a property rather than as an intention.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib.util
|
|
16
|
+
import pathlib
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _module():
|
|
25
|
+
spec = importlib.util.spec_from_file_location("changelog", ROOT / "scripts" / "changelog.py")
|
|
26
|
+
assert spec and spec.loader
|
|
27
|
+
mod = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(mod)
|
|
29
|
+
return mod
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
cl = _module()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _write(d: pathlib.Path, name: str, body: str) -> None:
|
|
36
|
+
(d / name).write_text(body)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_the_order_two_changes_land_in_does_not_reach_the_output(tmp_path) -> None:
|
|
40
|
+
"""The property the whole change exists for.
|
|
41
|
+
|
|
42
|
+
Two independent entries, folded in both possible orders, must produce the
|
|
43
|
+
same section — otherwise the changelog still carries a fact about merge
|
|
44
|
+
order that nobody needs and everybody has to resolve.
|
|
45
|
+
"""
|
|
46
|
+
first, second = tmp_path / "a", tmp_path / "b"
|
|
47
|
+
for d in (first, second):
|
|
48
|
+
d.mkdir()
|
|
49
|
+
_write(first, "fixed-alpha.md", "- Alpha was broken and is not.\n")
|
|
50
|
+
_write(first, "fixed-beta.md", "- Beta was broken and is not.\n")
|
|
51
|
+
# The same two, created in the opposite order.
|
|
52
|
+
_write(second, "fixed-beta.md", "- Beta was broken and is not.\n")
|
|
53
|
+
_write(second, "fixed-alpha.md", "- Alpha was broken and is not.\n")
|
|
54
|
+
|
|
55
|
+
a = cl.section("1.0.0", cl.read_fragments(first), "2026-01-01")
|
|
56
|
+
b = cl.section("1.0.0", cl.read_fragments(second), "2026-01-01")
|
|
57
|
+
assert a == b
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_kinds_are_grouped_in_keep_a_changelog_order(tmp_path) -> None:
|
|
61
|
+
_write(tmp_path, "fixed-one.md", "- A fix.\n")
|
|
62
|
+
_write(tmp_path, "added-two.md", "- An addition.\n")
|
|
63
|
+
_write(tmp_path, "notes-three.md", "- Some reasoning.\n")
|
|
64
|
+
|
|
65
|
+
out = cl.section("2.1.0", cl.read_fragments(tmp_path), "2026-01-01")
|
|
66
|
+
assert out.startswith("## [2.1.0] — 2026-01-01")
|
|
67
|
+
order = [m.group(1) for m in re.finditer(r"^### (\w+)", out, re.MULTILINE)]
|
|
68
|
+
assert order == ["Added", "Fixed", "Notes"], order
|
|
69
|
+
# Nothing empty is left standing.
|
|
70
|
+
assert "### Changed" not in out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_a_misnamed_fragment_is_refused_rather_than_dropped(tmp_path) -> None:
|
|
74
|
+
"""A changelog that quietly loses an entry is worse than one that refuses
|
|
75
|
+
to build — the entry is somebody's account of what they did to the money."""
|
|
76
|
+
_write(tmp_path, "fxied-typo.md", "- Something real.\n")
|
|
77
|
+
with pytest.raises(SystemExit, match="unknown kind"):
|
|
78
|
+
cl.read_fragments(tmp_path)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_the_new_section_goes_above_the_ones_already_there(tmp_path) -> None:
|
|
82
|
+
existing = "# Changelog\n\nPreamble.\n\n## [0.9.0] — 2025-01-01\n\n### Fixed\n\n- Older.\n"
|
|
83
|
+
out = cl.fold(existing, "## [1.0.0] — 2026-01-01\n\n### Added\n\n- Newer.\n")
|
|
84
|
+
assert out.index("## [1.0.0]") < out.index("## [0.9.0]")
|
|
85
|
+
assert "Preamble." in out and "- Older." in out
|
|
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
|
|
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
|
|
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
|
|
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
|