pullup 0.0.12__tar.gz → 0.0.13__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.
- {pullup-0.0.12 → pullup-0.0.13}/PKG-INFO +1 -1
- {pullup-0.0.12 → pullup-0.0.13}/pullup/__init__.py +1 -1
- {pullup-0.0.12 → pullup-0.0.13}/pullup/_modidx.py +11 -1
- {pullup-0.0.12 → pullup-0.0.13}/pullup/pipeline.py +87 -5
- {pullup-0.0.12 → pullup-0.0.13}/.gitignore +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/LICENSE +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/README.md +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/deploy.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/drive.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/env.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/infra.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/project.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/stack.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/wfbuild.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pullup/workflows.py +0 -0
- {pullup-0.0.12 → pullup-0.0.13}/pyproject.toml +0 -0
|
@@ -89,10 +89,20 @@ d = { 'settings': { 'branch': 'main',
|
|
|
89
89
|
'pullup.pipeline.Pipeline.write': ('pipeline.html#pipeline.write', 'pullup/pipeline.py'),
|
|
90
90
|
'pullup.pipeline.PipelineError': ('pipeline.html#pipelineerror', 'pullup/pipeline.py'),
|
|
91
91
|
'pullup.pipeline.Release': ('pipeline.html#release', 'pullup/pipeline.py'),
|
|
92
|
+
'pullup.pipeline.Release._row': ('pipeline.html#release._row', 'pullup/pipeline.py'),
|
|
93
|
+
'pullup.pipeline.Release._run': ('pipeline.html#release._run', 'pullup/pipeline.py'),
|
|
92
94
|
'pullup.pipeline.Release.defaults': ('pipeline.html#release.defaults', 'pullup/pipeline.py'),
|
|
93
95
|
'pullup.pipeline.Release.state': ('pipeline.html#release.state', 'pullup/pipeline.py'),
|
|
96
|
+
'pullup.pipeline._is_ver': ('pipeline.html#_is_ver', 'pullup/pipeline.py'),
|
|
97
|
+
'pullup.pipeline._sections': ('pipeline.html#_sections', 'pullup/pipeline.py'),
|
|
98
|
+
'pullup.pipeline.changelog_state': ('pipeline.html#changelog_state', 'pullup/pipeline.py'),
|
|
99
|
+
'pullup.pipeline.claim_unreleased': ('pipeline.html#claim_unreleased', 'pullup/pipeline.py'),
|
|
100
|
+
'pullup.pipeline.notes_step': ('pipeline.html#notes_step', 'pullup/pipeline.py'),
|
|
101
|
+
'pullup.pipeline.project_version': ('pipeline.html#project_version', 'pullup/pipeline.py'),
|
|
102
|
+
'pullup.pipeline.quiet': ('pipeline.html#quiet', 'pullup/pipeline.py'),
|
|
94
103
|
'pullup.pipeline.resolve_script': ('pipeline.html#resolve_script', 'pullup/pipeline.py'),
|
|
95
|
-
'pullup.pipeline.uv_argv': ('pipeline.html#uv_argv', 'pullup/pipeline.py')
|
|
104
|
+
'pullup.pipeline.uv_argv': ('pipeline.html#uv_argv', 'pullup/pipeline.py'),
|
|
105
|
+
'pullup.pipeline.write_changelog': ('pipeline.html#write_changelog', 'pullup/pipeline.py')},
|
|
96
106
|
'pullup.project': { 'pullup.project.Project': ('project.html#project', 'pullup/project.py'),
|
|
97
107
|
'pullup.project.Project.__init__': ('project.html#project.__init__', 'pullup/project.py'),
|
|
98
108
|
'pullup.project.Project.__repr__': ('project.html#project.__repr__', 'pullup/project.py'),
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
# %% ../nbs/02_pipeline.ipynb #d7426f18
|
|
4
4
|
from __future__ import annotations
|
|
5
|
-
import asyncio, json, os, shlex, time
|
|
5
|
+
import asyncio, json, os, re, shlex, time
|
|
6
|
+
from dataclasses import replace
|
|
6
7
|
from shutil import which
|
|
7
8
|
from fastcore.all import L, Path, first
|
|
8
9
|
from fastcore.ansi import strip_ansi
|
|
9
10
|
|
|
10
11
|
from .env import EnvStore, venv_env
|
|
11
|
-
from .project import Step, default_steps, release_flow, app_project
|
|
12
|
+
from .project import Step, default_steps, release_flow, app_project, _toml
|
|
12
13
|
|
|
13
14
|
# %% auto #0
|
|
14
|
-
__all__ = ['SCROLLBACK', 'DIR', 'PipelineError', 'resolve_script', 'uv_argv', 'Pipeline',
|
|
15
|
+
__all__ = ['SCROLLBACK', 'DIR', 'NOTES_CMDS', 'QUIET', 'MARKER', 'PipelineError', 'resolve_script', 'uv_argv', 'Pipeline',
|
|
16
|
+
'project_version', 'changelog_state', 'claim_unreleased', 'write_changelog', 'notes_step', 'quiet',
|
|
17
|
+
'Release']
|
|
15
18
|
|
|
16
19
|
# %% ../nbs/02_pipeline.ipynb #18ee2a74
|
|
17
20
|
SCROLLBACK = 200_000
|
|
@@ -108,6 +111,7 @@ class Pipeline:
|
|
|
108
111
|
try: env.update({k: str(v) for k, v in self.env.values(self.needs()).items()})
|
|
109
112
|
except Exception: pass
|
|
110
113
|
env.setdefault('PYTHONUNBUFFERED', '1')
|
|
114
|
+
if env.get('TERM') in (None, '', 'dumb'): env['TERM'] = 'xterm-256color'
|
|
111
115
|
return env
|
|
112
116
|
def _row(self, s):
|
|
113
117
|
"One step as a caller sees it: the plan, plus how the last run of it went."
|
|
@@ -235,6 +239,67 @@ class Pipeline:
|
|
|
235
239
|
return {}
|
|
236
240
|
return attribute(self.tail(400), checkouts=checkouts, family=family)
|
|
237
241
|
|
|
242
|
+
# %% ../nbs/02_pipeline.ipynb #7a1bd528
|
|
243
|
+
NOTES_CMDS = ('nbdev-release-gh', 'nbdev_release_gh')
|
|
244
|
+
QUIET = '--no-changelog --no-editor --yes'
|
|
245
|
+
MARKER = '<!-- do not remove -->\n'
|
|
246
|
+
|
|
247
|
+
def project_version(root):
|
|
248
|
+
"The version the next release carries, from `settings.ini` or else `pyproject.toml`; `''` when neither names one."
|
|
249
|
+
try: text = (Path(root)/'settings.ini').read_text(encoding='utf-8', errors='replace')
|
|
250
|
+
except OSError: text = ''
|
|
251
|
+
if m := re.search(r'^version\s*=\s*(\S+)', text, re.M): return m[1]
|
|
252
|
+
return str((_toml(root).get('project') or {}).get('version') or '')
|
|
253
|
+
|
|
254
|
+
def _sections(txt):
|
|
255
|
+
"The `## ` sections of a changelog in order, as `(heading, body)`."
|
|
256
|
+
return [(h.strip(), body) for h, _, body in (p.partition('\n') for p in re.split(r'^## ', txt, flags=re.M)[1:])]
|
|
257
|
+
|
|
258
|
+
def _is_ver(heading, ver): return bool(ver) and re.fullmatch(rf'v?{re.escape(ver)}(?![\w.])(?:[\s(\[-].*)?', heading) is not None
|
|
259
|
+
|
|
260
|
+
def changelog_state(root, version=None):
|
|
261
|
+
"Whether the first section of `CHANGELOG.md`, the one nbdev publishes, is a filled entry for `version` (default: the project's)."
|
|
262
|
+
root = Path(root); version = version or project_version(root)
|
|
263
|
+
out = dict(version=version, path=str(root/'CHANGELOG.md'), has=False, kind='', heading=f'## {version}')
|
|
264
|
+
try: secs = _sections((root/'CHANGELOG.md').read_text(encoding='utf-8'))
|
|
265
|
+
except OSError: return out
|
|
266
|
+
if not secs or not secs[0][1].strip() or not version: return out
|
|
267
|
+
if _is_ver(secs[0][0], version): out |= dict(has=True, kind='version')
|
|
268
|
+
elif secs[0][0].lower() == 'unreleased': out |= dict(has=True, kind='unreleased')
|
|
269
|
+
return out
|
|
270
|
+
|
|
271
|
+
def claim_unreleased(root, version):
|
|
272
|
+
"Rename the leading `## Unreleased` heading to `## version`; returns the path, or None when there is no such heading."
|
|
273
|
+
path = Path(root)/'CHANGELOG.md'
|
|
274
|
+
try: txt = path.read_text(encoding='utf-8')
|
|
275
|
+
except OSError: return None
|
|
276
|
+
new, n = re.subn(r'^## Unreleased[ \t]*$', f'## {version}', txt, count=1, flags=re.M | re.I)
|
|
277
|
+
if n: path.write_text(new, encoding='utf-8')
|
|
278
|
+
return path if n else None
|
|
279
|
+
|
|
280
|
+
def write_changelog(root, version, text):
|
|
281
|
+
"Write `text` as the entry for `version`, where nbdev puts one; an entry already there is replaced. Returns the path."
|
|
282
|
+
text, path = str(text or '').strip(), Path(root)/'CHANGELOG.md'
|
|
283
|
+
if not version: raise PipelineError('there is no version to write release notes for')
|
|
284
|
+
if not text: raise PipelineError('the release notes are empty')
|
|
285
|
+
old = path.read_text(encoding='utf-8') if path.exists() else f'# Release notes\n\n{MARKER}'
|
|
286
|
+
old = re.sub(rf'^## v?{re.escape(version)}(?![\w.])[^\n]*(?:\n|\Z).*?(?=^## |\Z)', '', old, flags=re.M | re.S)
|
|
287
|
+
entry = f'## {version}\n\n{text}\n\n'
|
|
288
|
+
if MARKER in old: head, _, rest = old.partition(MARKER); head += MARKER + '\n'
|
|
289
|
+
elif m := re.search(r'^## ', old, re.M): head, rest = old[:m.start()].rstrip('\n') + '\n\n', old[m.start():]
|
|
290
|
+
else: head, rest = (old.rstrip('\n') + '\n\n' if old.strip() else ''), ''
|
|
291
|
+
path.write_text((head + entry + rest.lstrip('\n')).rstrip('\n') + '\n', encoding='utf-8')
|
|
292
|
+
return path
|
|
293
|
+
|
|
294
|
+
def notes_step(step):
|
|
295
|
+
"Whether `step` is the bare nbdev release command, which stops to ask questions and open an editor."
|
|
296
|
+
return not step.argv and step.cmd.strip() in NOTES_CMDS
|
|
297
|
+
|
|
298
|
+
def quiet(cmd):
|
|
299
|
+
"The nbdev release command with the flags that stop it asking for an editor or a yes."
|
|
300
|
+
return f'{cmd.strip()} {QUIET}'
|
|
301
|
+
|
|
302
|
+
|
|
238
303
|
# %% ../nbs/02_pipeline.ipynb #e22f9231
|
|
239
304
|
class Release(Pipeline):
|
|
240
305
|
"The release pipeline, whose default plan is the project's release flow."
|
|
@@ -242,7 +307,24 @@ class Release(Pipeline):
|
|
|
242
307
|
KIND = 'release'
|
|
243
308
|
@staticmethod
|
|
244
309
|
def defaults(root): return default_steps(root)
|
|
310
|
+
def _row(self, s):
|
|
311
|
+
row = super()._row(s)
|
|
312
|
+
return row | ({'notes': (self.results.get(s.id) or {}).get('notes')} if row['status'] == 'needs_notes' else {})
|
|
245
313
|
def state(self):
|
|
246
314
|
flow = release_flow(self.root)
|
|
247
|
-
|
|
248
|
-
|
|
315
|
+
st = super().state()
|
|
316
|
+
return st | {'flow': flow, 'nbdev': flow == 'nbdev',
|
|
317
|
+
'fastship': flow == 'fastship', 'app': app_project(self.root),
|
|
318
|
+
'notes': first((r['notes'] for r in st['steps'] if r['status'] == 'needs_notes'))}
|
|
319
|
+
async def _run(self, step):
|
|
320
|
+
"Run `step`; the bare nbdev release command runs quiet once the changelog has its entry, and otherwise waits for notes."
|
|
321
|
+
if not notes_step(step): return await super()._run(step)
|
|
322
|
+
st = changelog_state(self.root)
|
|
323
|
+
if not st['has']:
|
|
324
|
+
self._auto = False
|
|
325
|
+
self.results[step.id] = {'status': 'needs_notes', 'code': None, 'started': 0, 'finished': 0, 'notes': st}
|
|
326
|
+
self._broadcast(f"\r\n\x1b[33m{step.label} waits for release notes for {st['version'] or 'this version'}\x1b[0m\r\n".encode())
|
|
327
|
+
return
|
|
328
|
+
if st['kind'] == 'unreleased': claim_unreleased(self.root, st['version'])
|
|
329
|
+
await super()._run(replace(step, cmd=quiet(step.cmd)))
|
|
330
|
+
|
|
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
|