pullup 0.0.12__tar.gz → 0.0.14__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.14}/PKG-INFO +1 -1
- {pullup-0.0.12 → pullup-0.0.14}/pullup/__init__.py +1 -1
- {pullup-0.0.12 → pullup-0.0.14}/pullup/_modidx.py +13 -1
- {pullup-0.0.12 → pullup-0.0.14}/pullup/pipeline.py +98 -5
- {pullup-0.0.12 → pullup-0.0.14}/.gitignore +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/LICENSE +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/README.md +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/deploy.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/drive.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/env.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/infra.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/project.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/stack.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/wfbuild.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pullup/workflows.py +0 -0
- {pullup-0.0.12 → pullup-0.0.14}/pyproject.toml +0 -0
|
@@ -89,10 +89,22 @@ 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'),
|
|
103
|
+
'pullup.pipeline.release_cmd': ('pipeline.html#release_cmd', 'pullup/pipeline.py'),
|
|
94
104
|
'pullup.pipeline.resolve_script': ('pipeline.html#resolve_script', 'pullup/pipeline.py'),
|
|
95
|
-
'pullup.pipeline.
|
|
105
|
+
'pullup.pipeline.tree_clean': ('pipeline.html#tree_clean', 'pullup/pipeline.py'),
|
|
106
|
+
'pullup.pipeline.uv_argv': ('pipeline.html#uv_argv', 'pullup/pipeline.py'),
|
|
107
|
+
'pullup.pipeline.write_changelog': ('pipeline.html#write_changelog', 'pullup/pipeline.py')},
|
|
96
108
|
'pullup.project': { 'pullup.project.Project': ('project.html#project', 'pullup/project.py'),
|
|
97
109
|
'pullup.project.Project.__init__': ('project.html#project.__init__', 'pullup/project.py'),
|
|
98
110
|
'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, subprocess, 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', '
|
|
15
|
+
__all__ = ['SCROLLBACK', 'DIR', 'NOTES_CMDS', 'QUIET', 'CLEAN_CMD', 'MARKER', 'PipelineError', 'resolve_script', 'uv_argv',
|
|
16
|
+
'Pipeline', 'project_version', 'changelog_state', 'claim_unreleased', 'write_changelog', 'notes_step',
|
|
17
|
+
'quiet', 'tree_clean', 'release_cmd', '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,78 @@ 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
|
+
CLEAN_CMD = 'nbdev-release-git'
|
|
246
|
+
MARKER = '<!-- do not remove -->\n'
|
|
247
|
+
|
|
248
|
+
def project_version(root):
|
|
249
|
+
"The version the next release carries, from `settings.ini` or else `pyproject.toml`; `''` when neither names one."
|
|
250
|
+
try: text = (Path(root)/'settings.ini').read_text(encoding='utf-8', errors='replace')
|
|
251
|
+
except OSError: text = ''
|
|
252
|
+
if m := re.search(r'^version\s*=\s*(\S+)', text, re.M): return m[1]
|
|
253
|
+
return str((_toml(root).get('project') or {}).get('version') or '')
|
|
254
|
+
|
|
255
|
+
def _sections(txt):
|
|
256
|
+
"The `## ` sections of a changelog in order, as `(heading, body)`."
|
|
257
|
+
return [(h.strip(), body) for h, _, body in (p.partition('\n') for p in re.split(r'^## ', txt, flags=re.M)[1:])]
|
|
258
|
+
|
|
259
|
+
def _is_ver(heading, ver): return bool(ver) and re.fullmatch(rf'v?{re.escape(ver)}(?![\w.])(?:[\s(\[-].*)?', heading) is not None
|
|
260
|
+
|
|
261
|
+
def changelog_state(root, version=None):
|
|
262
|
+
"Whether the first section of `CHANGELOG.md`, the one nbdev publishes, is a filled entry for `version` (default: the project's)."
|
|
263
|
+
root = Path(root); version = version or project_version(root)
|
|
264
|
+
out = dict(version=version, path=str(root/'CHANGELOG.md'), has=False, kind='', heading=f'## {version}')
|
|
265
|
+
try: secs = _sections((root/'CHANGELOG.md').read_text(encoding='utf-8'))
|
|
266
|
+
except OSError: return out
|
|
267
|
+
if not secs or not secs[0][1].strip() or not version: return out
|
|
268
|
+
if _is_ver(secs[0][0], version): out |= dict(has=True, kind='version')
|
|
269
|
+
elif secs[0][0].lower() == 'unreleased': out |= dict(has=True, kind='unreleased')
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
def claim_unreleased(root, version):
|
|
273
|
+
"Rename the leading `## Unreleased` heading to `## version`; returns the path, or None when there is no such heading."
|
|
274
|
+
path = Path(root)/'CHANGELOG.md'
|
|
275
|
+
try: txt = path.read_text(encoding='utf-8')
|
|
276
|
+
except OSError: return None
|
|
277
|
+
new, n = re.subn(r'^## Unreleased[ \t]*$', f'## {version}', txt, count=1, flags=re.M | re.I)
|
|
278
|
+
if n: path.write_text(new, encoding='utf-8')
|
|
279
|
+
return path if n else None
|
|
280
|
+
|
|
281
|
+
def write_changelog(root, version, text):
|
|
282
|
+
"Write `text` as the entry for `version`, where nbdev puts one; an entry already there is replaced. Returns the path."
|
|
283
|
+
text, path = str(text or '').strip(), Path(root)/'CHANGELOG.md'
|
|
284
|
+
if not version: raise PipelineError('there is no version to write release notes for')
|
|
285
|
+
if not text: raise PipelineError('the release notes are empty')
|
|
286
|
+
old = path.read_text(encoding='utf-8') if path.exists() else f'# Release notes\n\n{MARKER}'
|
|
287
|
+
old = re.sub(rf'^## v?{re.escape(version)}(?![\w.])[^\n]*(?:\n|\Z).*?(?=^## |\Z)', '', old, flags=re.M | re.S)
|
|
288
|
+
entry = f'## {version}\n\n{text}\n\n'
|
|
289
|
+
if MARKER in old: head, _, rest = old.partition(MARKER); head += MARKER + '\n'
|
|
290
|
+
elif m := re.search(r'^## ', old, re.M): head, rest = old[:m.start()].rstrip('\n') + '\n\n', old[m.start():]
|
|
291
|
+
else: head, rest = (old.rstrip('\n') + '\n\n' if old.strip() else ''), ''
|
|
292
|
+
path.write_text((head + entry + rest.lstrip('\n')).rstrip('\n') + '\n', encoding='utf-8')
|
|
293
|
+
return path
|
|
294
|
+
|
|
295
|
+
def notes_step(step):
|
|
296
|
+
"Whether `step` is the bare nbdev release command, which stops to ask questions and open an editor."
|
|
297
|
+
return not step.argv and step.cmd.strip() in NOTES_CMDS
|
|
298
|
+
|
|
299
|
+
def quiet(cmd):
|
|
300
|
+
"The nbdev release command with the flags that stop it asking for an editor or a yes."
|
|
301
|
+
return f'{cmd.strip()} {QUIET}'
|
|
302
|
+
|
|
303
|
+
def tree_clean(root):
|
|
304
|
+
"Whether git has no tracked change for `nbdev-release-gh` to commit; a folder git cannot read counts as not clean."
|
|
305
|
+
try: r = subprocess.run(['git', 'status', '--porcelain', '--untracked-files=no'], cwd=str(root), capture_output=True, text=True, stdin=subprocess.DEVNULL)
|
|
306
|
+
except OSError: return False
|
|
307
|
+
return r.returncode == 0 and not r.stdout.strip()
|
|
308
|
+
|
|
309
|
+
def release_cmd(root, cmd):
|
|
310
|
+
"`nbdev-release-git` when there is nothing to commit, as `nbdev-release-gh` would stop there; else the quiet `cmd`."
|
|
311
|
+
return CLEAN_CMD if tree_clean(root) else quiet(cmd)
|
|
312
|
+
|
|
313
|
+
|
|
238
314
|
# %% ../nbs/02_pipeline.ipynb #e22f9231
|
|
239
315
|
class Release(Pipeline):
|
|
240
316
|
"The release pipeline, whose default plan is the project's release flow."
|
|
@@ -242,7 +318,24 @@ class Release(Pipeline):
|
|
|
242
318
|
KIND = 'release'
|
|
243
319
|
@staticmethod
|
|
244
320
|
def defaults(root): return default_steps(root)
|
|
321
|
+
def _row(self, s):
|
|
322
|
+
row = super()._row(s)
|
|
323
|
+
return row | ({'notes': (self.results.get(s.id) or {}).get('notes')} if row['status'] == 'needs_notes' else {})
|
|
245
324
|
def state(self):
|
|
246
325
|
flow = release_flow(self.root)
|
|
247
|
-
|
|
248
|
-
|
|
326
|
+
st = super().state()
|
|
327
|
+
return st | {'flow': flow, 'nbdev': flow == 'nbdev',
|
|
328
|
+
'fastship': flow == 'fastship', 'app': app_project(self.root),
|
|
329
|
+
'notes': first((r['notes'] for r in st['steps'] if r['status'] == 'needs_notes'))}
|
|
330
|
+
async def _run(self, step):
|
|
331
|
+
"Run `step`; the bare nbdev release command runs quiet once the changelog has its entry (as `nbdev-release-git` when the tree is clean), and otherwise waits for notes."
|
|
332
|
+
if not notes_step(step): return await super()._run(step)
|
|
333
|
+
st = changelog_state(self.root)
|
|
334
|
+
if not st['has']:
|
|
335
|
+
self._auto = False
|
|
336
|
+
self.results[step.id] = {'status': 'needs_notes', 'code': None, 'started': 0, 'finished': 0, 'notes': st}
|
|
337
|
+
self._broadcast(f"\r\n\x1b[33m{step.label} waits for release notes for {st['version'] or 'this version'}\x1b[0m\r\n".encode())
|
|
338
|
+
return
|
|
339
|
+
if st['kind'] == 'unreleased': claim_unreleased(self.root, st['version'])
|
|
340
|
+
await super()._run(replace(step, cmd=release_cmd(self.root, step.cmd)))
|
|
341
|
+
|
|
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
|