pullup 0.0.5__tar.gz → 0.0.6__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.
@@ -0,0 +1,153 @@
1
+ _docs/
2
+ _proc/
3
+ sidebar.yml
4
+
5
+ *.bak
6
+ .gitattributes
7
+ .last_checked
8
+ .gitconfig
9
+ *.bak
10
+ *.log
11
+ *~
12
+ ~*
13
+ _tmp*
14
+ tmp*
15
+ tags
16
+ *.pkg
17
+
18
+ # Byte-compiled / optimized / DLL files
19
+ __pycache__/
20
+ *.py[cod]
21
+ *$py.class
22
+
23
+ # C extensions
24
+ *.so
25
+
26
+ # Distribution / packaging
27
+ .Python
28
+ env/
29
+ build/
30
+ conda/
31
+ develop-eggs/
32
+ dist/
33
+ downloads/
34
+ eggs/
35
+ .eggs/
36
+ lib/
37
+ lib64/
38
+ parts/
39
+ sdist/
40
+ var/
41
+ wheels/
42
+ *.egg-info/
43
+ .installed.cfg
44
+ *.egg
45
+
46
+ # PyInstaller
47
+ # Usually these files are written by a python script from a template
48
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
49
+ *.manifest
50
+ *.spec
51
+
52
+ # Installer logs
53
+ pip-log.txt
54
+ pip-delete-this-directory.txt
55
+
56
+ # Unit test / coverage reports
57
+ htmlcov/
58
+ .tox/
59
+ .coverage
60
+ .coverage.*
61
+ .cache
62
+ nosetests.xml
63
+ coverage.xml
64
+ *.cover
65
+ .hypothesis/
66
+
67
+ # Translations
68
+ *.mo
69
+ *.pot
70
+
71
+ # Django stuff:
72
+ *.log
73
+ local_settings.py
74
+
75
+ # Flask stuff:
76
+ instance/
77
+ .webassets-cache
78
+
79
+ # Scrapy stuff:
80
+ .scrapy
81
+
82
+ # Sphinx documentation
83
+ docs/_build/
84
+
85
+ # PyBuilder
86
+ target/
87
+
88
+ # Jupyter Notebook
89
+ .ipynb_checkpoints
90
+
91
+ # pyenv
92
+ .python-version
93
+
94
+ # celery beat schedule file
95
+ celerybeat-schedule
96
+
97
+ # SageMath parsed files
98
+ *.sage.py
99
+
100
+ # dotenv
101
+ .env
102
+
103
+ # virtualenv
104
+ .venv
105
+ venv/
106
+ ENV/
107
+
108
+ # Spyder project settings
109
+ .spyderproject
110
+ .spyproject
111
+
112
+ # Rope project settings
113
+ .ropeproject
114
+
115
+ # mkdocs documentation
116
+ /site
117
+
118
+ # mypy
119
+ .mypy_cache/
120
+
121
+ .vscode
122
+ *.swp
123
+
124
+ # osx generated files
125
+ .DS_Store
126
+ .DS_Store?
127
+ .Trashes
128
+ ehthumbs.db
129
+ Thumbs.db
130
+ .idea
131
+
132
+ # pytest
133
+ .pytest_cache
134
+
135
+ # tools/trust-doc-nbs
136
+ docs_src/.last_checked
137
+
138
+ # symlinks to fastai
139
+ docs_src/fastai
140
+ tools/fastai
141
+
142
+ # link checker
143
+ checklink/cookies.txt
144
+
145
+ # .gitconfig is now autogenerated
146
+ .gitconfig
147
+
148
+ # Quarto installer
149
+ .deb
150
+ .pkg
151
+
152
+ # Quarto
153
+ .quarto
@@ -1,30 +1,29 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pullup
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: build, release, and deploy projects
5
- Author-email: Karthik Rajgopal <karthik.rajgopal@hotmail.com>
6
- License: Apache-2.0
7
5
  Project-URL: Repository, https://github.com/vedicreader/pullup
8
6
  Project-URL: Documentation, https://vedicreader.github.io/pullup
9
- Keywords: nbdev,jupyter,notebook,python
10
- Classifier: Natural Language :: English
11
- Classifier: Intended Audience :: Developers
7
+ Author-email: Karthik Rajgopal <karthik.rajgopal@hotmail.com>
8
+ License: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: jupyter,nbdev,notebook,python
12
11
  Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
13
14
  Classifier: Programming Language :: Python :: 3
14
15
  Classifier: Programming Language :: Python :: 3 :: Only
15
16
  Requires-Python: >=3.11
16
- Description-Content-Type: text/markdown
17
- License-File: LICENSE
17
+ Requires-Dist: cfeasy>=0.0.4
18
+ Requires-Dist: dockeasy>=0.0.5
18
19
  Requires-Dist: fastcore>=1.8.17
20
+ Requires-Dist: gheasy>=0.0.10
21
+ Requires-Dist: keyring>=25.0.0
19
22
  Requires-Dist: ptymini>=0.0.5
23
+ Requires-Dist: python-dotenv>=1.0.0
20
24
  Requires-Dist: pyyaml>=6.0
21
- Requires-Dist: dockeasy>=0.0.5
22
- Requires-Dist: gheasy>=0.0.10
23
25
  Requires-Dist: vpseasy>=0.0.18
24
- Requires-Dist: cfeasy>=0.0.4
25
- Requires-Dist: python-dotenv>=1.0.0
26
- Requires-Dist: keyring>=25.0.0
27
- Dynamic: license-file
26
+ Description-Content-Type: text/markdown
28
27
 
29
28
  # pullup
30
29
 
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.5"
1
+ __version__ = "0.0.6"
2
2
 
3
3
  # The names a caller reaches for. The modules stay importable for everything else.
4
4
  from .env import EnvError, EnvStore, venv_env
@@ -1,6 +1,6 @@
1
1
  [build-system]
2
- requires = ["setuptools>=64"]
3
- build-backend = "setuptools.build_meta"
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pullup"
@@ -23,11 +23,14 @@ Documentation = "https://vedicreader.github.io/pullup"
23
23
  [project.entry-points.nbdev]
24
24
  pullup = "pullup._modidx:d"
25
25
 
26
- [tool.setuptools.dynamic]
27
- version = {attr = "pullup.__version__"}
26
+ [tool.hatch.version]
27
+ path = "pullup/__init__.py"
28
28
 
29
- [tool.setuptools.packages.find]
30
- include = ["pullup"]
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["pullup"]
31
+
32
+ [tool.hatch.build.targets.sdist]
33
+ include = ["/pullup", "/README.md", "/pyproject.toml"]
31
34
 
32
35
  [tool.nbdev]
33
36
  doc_host = "https://vedicreader.github.io"
@@ -38,4 +41,4 @@ testpaths = ["tests"]
38
41
  asyncio_mode = "auto"
39
42
 
40
43
  [dependency-groups]
41
- dev = ['pytest>=8.0.0', 'pytest-asyncio>=1.0.0', 'nbdev>=3.3.11']
44
+ dev = ['pytest>=8.0.0', 'pytest-asyncio>=1.0.0', 'nbdev>=3.3.11']
pullup-0.0.5/MANIFEST.in DELETED
@@ -1,5 +0,0 @@
1
- include settings.ini
2
- include LICENSE
3
- include CONTRIBUTING.md
4
- include README.md
5
- recursive-exclude * __pycache__
pullup-0.0.5/PKG-INFO DELETED
@@ -1,163 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pullup
3
- Version: 0.0.5
4
- Summary: build, release, and deploy projects
5
- Author-email: Karthik Rajgopal <karthik.rajgopal@hotmail.com>
6
- License: Apache-2.0
7
- Project-URL: Repository, https://github.com/vedicreader/pullup
8
- Project-URL: Documentation, https://vedicreader.github.io/pullup
9
- Keywords: nbdev,jupyter,notebook,python
10
- Classifier: Natural Language :: English
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3 :: Only
15
- Requires-Python: >=3.11
16
- Description-Content-Type: text/markdown
17
- License-File: LICENSE
18
- Requires-Dist: fastcore>=1.8.17
19
- Requires-Dist: ptymini>=0.0.5
20
- Requires-Dist: pyyaml>=6.0
21
- Requires-Dist: dockeasy>=0.0.5
22
- Requires-Dist: gheasy>=0.0.10
23
- Requires-Dist: vpseasy>=0.0.18
24
- Requires-Dist: cfeasy>=0.0.4
25
- Requires-Dist: python-dotenv>=1.0.0
26
- Requires-Dist: keyring>=25.0.0
27
- Dynamic: license-file
28
-
29
- # pullup
30
-
31
-
32
- <!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
33
-
34
- ## Install
35
-
36
- ``` sh
37
- pip install pullup
38
- ```
39
-
40
- Cloud integrations load only when used.
41
-
42
- ## Project detection
43
-
44
- [`Project`](https://vedicreader.github.io/pullup/project.html#project) detects the release flow from files in the project directory.
45
-
46
- ``` python
47
- project = Project(root)
48
- project.kind, [step.id for step in project.steps()]
49
- ```
50
-
51
- ('nbdev', ['prepare', 'bump', 'gh', 'pypi'])
52
-
53
- ## Running steps
54
-
55
- Steps run in one PTY. Automatic execution stops at the first failure.
56
-
57
- ``` python
58
- pipeline = Pipeline(root, dir='.demo')
59
- pipeline.save([{'id': 'hello', 'cmd': 'say hello',
60
- 'argv': [sys.executable, '-c', 'print("ready")']}])
61
- await pipeline.start()
62
- pipeline.state()['done'], pipeline.tail().splitlines()[-2:]
63
- ```
64
-
65
- (False, ['', '❯ say hello'])
66
-
67
- ## Plan storage
68
-
69
- Plans are written only below the project directory.
70
-
71
- ``` python
72
- custom = Pipeline(root, dir='.myapp')
73
- custom.save([{'id': 'show', 'cmd': 'show', 'argv': [sys.executable, '-c', 'print("saved")']}])
74
- custom.path.relative_to(root)
75
- ```
76
-
77
- Path('.myapp/pipeline.json')
78
-
79
- ## Deployment pipelines
80
-
81
- [`Deploy`](https://vedicreader.github.io/pullup/deploy.html#deploy) runs a project’s deployment script. [`Drive`](https://vedicreader.github.io/pullup/drive.html#drive) exposes each operation as a library call.
82
-
83
- ``` python
84
- Deploy(root).state()['kind'], Drive(root).state()['kind']
85
- ```
86
-
87
- ('deploy', 'drive')
88
-
89
- ## Workflows
90
-
91
- Local workflow inspection needs no GitHub credentials.
92
-
93
- ``` python
94
- workflow_dir = root/'.github'/'workflows'
95
- workflow_dir.mkdir(parents=True)
96
- (workflow_dir/'ci.yml').write_text('''name: CI
97
- on: push
98
- jobs:
99
- test:
100
- runs-on: ubuntu-latest
101
- steps:
102
- - run: pytest
103
- ''')
104
- workflows = Workflows(root)
105
- [(row['name'], [job['id'] for job in row['jobs']]) for row in workflows.parsed()]
106
- ```
107
-
108
- [('CI', ['test'])]
109
-
110
- ## Environment
111
-
112
- A pipeline injects required values into the child without changing the parent environment.
113
-
114
- ``` python
115
- class Values:
116
- def get(self, key, secret=True): return 'demo-token' if key == 'DEMO_TOKEN' else ''
117
- def values(self, keys): return {key: self.get(key) for key in keys if self.get(key)}
118
-
119
- with_values = Pipeline(root, env=Values(), dir='.env-demo')
120
- with_values.save([{'id': 'env', 'cmd': 'read environment', 'needs': ['DEMO_TOKEN'],
121
- 'argv': [sys.executable, '-c', 'import os; print(os.environ["DEMO_TOKEN"])']}])
122
- await with_values.start()
123
- with_values.tail().splitlines()[-2:]
124
- ```
125
-
126
- ['', '❯ read environment']
127
-
128
- ## Package errors
129
-
130
- [`blame`](https://vedicreader.github.io/pullup/stack.html#blame) identifies the deepest selected-package frame.
131
-
132
- ``` python
133
- trace = '''Traceback (most recent call last):
134
- File "/tmp/pullup/demo.py", line 7, in release
135
- raise RuntimeError("stopped")
136
- RuntimeError: stopped
137
- '''
138
- blame(trace, family=['pullup'])
139
- ```
140
-
141
- {'package': 'pullup',
142
- 'file': '/tmp/pullup/demo.py',
143
- 'line': 7,
144
- 'fn': 'release',
145
- 'error': 'RuntimeError: stopped',
146
- 'open': '/tmp/pullup/demo.py',
147
- 'checkout': '',
148
- 'version': '0.0.4'}
149
-
150
- ## Infrastructure
151
-
152
- [`Infra.status`](https://vedicreader.github.io/pullup/infra.html#infra.status) reports provider and token availability without making a request.
153
-
154
- ``` python
155
- infra = Infra(env={})
156
- infra.status()
157
- ```
158
-
159
- {'vpseasy': True,
160
- 'cfeasy': True,
161
- 'hcloud_token': True,
162
- 'cf_token': False,
163
- 'keys': ['HCLOUD_TOKEN', 'CLOUDFLARE_API_TOKEN']}
@@ -1,26 +0,0 @@
1
- LICENSE
2
- MANIFEST.in
3
- README.md
4
- pyproject.toml
5
- pullup/__init__.py
6
- pullup/_modidx.py
7
- pullup/deploy.py
8
- pullup/drive.py
9
- pullup/env.py
10
- pullup/infra.py
11
- pullup/pipeline.py
12
- pullup/project.py
13
- pullup/stack.py
14
- pullup/wfbuild.py
15
- pullup/workflows.py
16
- pullup.egg-info/PKG-INFO
17
- pullup.egg-info/SOURCES.txt
18
- pullup.egg-info/dependency_links.txt
19
- pullup.egg-info/entry_points.txt
20
- pullup.egg-info/requires.txt
21
- pullup.egg-info/top_level.txt
22
- tests/test_env.py
23
- tests/test_pipeline.py
24
- tests/test_project.py
25
- tests/test_stack.py
26
- tests/test_workflows.py
@@ -1,2 +0,0 @@
1
- [nbdev]
2
- pullup = pullup._modidx:d
@@ -1,9 +0,0 @@
1
- fastcore>=1.8.17
2
- ptymini>=0.0.5
3
- pyyaml>=6.0
4
- dockeasy>=0.0.5
5
- gheasy>=0.0.10
6
- vpseasy>=0.0.18
7
- cfeasy>=0.0.4
8
- python-dotenv>=1.0.0
9
- keyring>=25.0.0
@@ -1 +0,0 @@
1
- pullup
pullup-0.0.5/setup.cfg DELETED
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
@@ -1,54 +0,0 @@
1
- "The environment a project's commands run in."
2
- import os
3
- from pathlib import Path
4
- import pytest
5
- from pullup.env import BUNDLE_ONLY, EnvStore, clean_env, env_value, strip_bundle, venv_env
6
- from pullup.deploy import Deploy
7
- from pullup.drive import Drive
8
-
9
- def test_outside_a_bundle_the_environment_is_handed_back_untouched():
10
- "A caller that claimed nothing about an environment still claims nothing."
11
- given = {'PYTHONHOME': '/somewhere', 'PATH': '/bin'}
12
- assert strip_bundle(dict(given), frozen=False) == given
13
-
14
- def test_inside_one_the_hosts_interpreter_redirection_is_dropped():
15
- """py2app points PYTHONHOME at the bundle. A child that keeps it imports the bundle's standard
16
- library under another interpreter and dies somewhere that names nothing to do with the cause."""
17
- out = strip_bundle({n: 'x' for n in BUNDLE_ONLY} | {'PATH': '/bin'}, frozen=True)
18
- assert not (set(out) & set(BUNDLE_ONLY)) and out['PATH'] == '/bin'
19
- assert out['PYTHONUTF8'] == '1'
20
-
21
- def test_naming_an_interpreter_puts_its_environment_in_front(tmp_path):
22
- venv = tmp_path/'.venv'; (venv/'bin').mkdir(parents=True)
23
- py = venv/'bin'/'python'; py.write_text('')
24
- env = venv_env(py, env={'PATH': '/usr/bin', 'UV_PROJECT_ENVIRONMENT': '/elsewhere'})
25
- assert env['VIRTUAL_ENV'] == str(venv)
26
- assert env['PATH'].startswith(str(venv/'bin') + os.pathsep)
27
- assert 'UV_PROJECT_ENVIRONMENT' not in env, 'it is read where the process ends up, not here'
28
- assert 'PYTHONHOME' not in env
29
-
30
- def test_naming_no_interpreter_still_gives_a_usable_environment():
31
- assert venv_env(None, env={'PATH': '/bin'}) == {'PATH': '/bin'}
32
- assert 'PATH' in venv_env()
33
-
34
- def test_a_value_that_cannot_be_read_falls_back_rather_than_raising():
35
- "A missing dockeasy is a reason to use the default, not to fail the step that wanted it."
36
- class Broken:
37
- def get(self, key, secret=False): raise RuntimeError('no store here')
38
- assert env_value(Broken(), 'ANYTHING', 'fallback') == 'fallback'
39
- assert env_value(Broken(), 'ANYTHING') == ''
40
-
41
- def test_a_store_with_no_dockeasy_says_which_extra_installs_it():
42
- with pytest.raises(Exception, match='pullup'):
43
- EnvStore().get('SOMETHING') if not _dockeasy_installed() else pytest.skip('dockeasy is here')
44
-
45
- def _dockeasy_installed():
46
- from importlib.util import find_spec
47
- try: return find_spec('dockeasy') is not None
48
- except (ImportError, ValueError): return False
49
-
50
- def test_every_pipeline_kind_writes_under_the_directory_it_was_given(tmp_path):
51
- "Three kinds, one rule: the caller names the folder, and nothing writes outside it."
52
- for kind, name in ((Deploy, 'deploy.json'), (Drive, 'drive.json')):
53
- p = kind(tmp_path, dir='.somewhere')
54
- assert p.path == tmp_path/'.somewhere'/name
@@ -1,103 +0,0 @@
1
- "The plan, the state, and a step that actually runs on a pty."
2
- import asyncio, json
3
- import pytest
4
- from pullup.project import Step
5
- from pullup.pipeline import Pipeline, PipelineError, resolve_script, uv_argv
6
-
7
- class Steps(Pipeline):
8
- "A pipeline whose plan is given rather than sniffed from the folder."
9
- plan = ()
10
- @classmethod
11
- def defaults(cls, root): return list(cls.plan)
12
-
13
- def mk(tmp_path, steps, **kw):
14
- Steps.plan = steps
15
- return Steps(tmp_path, **kw)
16
-
17
- def test_the_plan_is_written_where_the_caller_says_and_nowhere_else(tmp_path):
18
- "A package that wrote into a directory named after one of its callers would be one to fork."
19
- p = mk(tmp_path, [Step('a', 'a', 'true')], dir='.mine')
20
- assert p.path == tmp_path/'.mine'/'pipeline.json'
21
- p.save([{'id': 'b', 'label': 'b', 'cmd': 'false'}])
22
- assert json.loads(p.path.read_text())['steps'][0]['cmd'] == 'false'
23
- assert [s.id for s in Steps(tmp_path, dir='.mine').steps] == ['b'], 'and it is read back'
24
- assert not (tmp_path/'.leela').exists() and not (tmp_path/'.pullup').exists()
25
-
26
- def test_a_plan_with_no_command_in_it_is_refused(tmp_path):
27
- p = mk(tmp_path, [Step('a', 'a', 'true')])
28
- with pytest.raises(PipelineError, match='at least one step'): p.save([{'id': 'x', 'cmd': ' '}])
29
- with pytest.raises(PipelineError, match='at least one step'): p.save([])
30
-
31
- def test_an_unreadable_plan_falls_back_to_the_default_rather_than_raising(tmp_path):
32
- "A half-written file is a reason to offer the default plan, not to make the panel unopenable."
33
- (tmp_path/'.pullup').mkdir()
34
- (tmp_path/'.pullup'/'pipeline.json').write_text('{ not json')
35
- assert [s.id for s in mk(tmp_path, [Step('a', 'a', 'true')]).steps] == ['a']
36
-
37
- def test_the_state_says_what_ran_what_is_running_and_what_is_still_wanted(tmp_path):
38
- p = mk(tmp_path, [Step('a', 'a', 'true'), Step('b', 'b', 'true', needs=['NOPE_NOT_SET'])])
39
- st = p.state()
40
- assert st['kind'] == 'pipeline' and not st['configured'] and not st['running']
41
- assert [r['status'] for r in st['steps']] == ['pending', 'pending']
42
- assert st['needs'] == ['NOPE_NOT_SET'] and st['steps'][1]['missing'] == ['NOPE_NOT_SET']
43
- assert not st['done'] and st['failed'] == ''
44
- p.skip('a'); p.skip('b')
45
- assert p.state()['done'], 'skipped counts as done: it is a decision, not a gap'
46
- p.reset()
47
- assert [r['status'] for r in p.state()['steps']] == ['pending', 'pending']
48
-
49
- def test_an_unknown_step_is_named_rather_than_silently_skipped(tmp_path):
50
- p = mk(tmp_path, [Step('a', 'a', 'true')])
51
- with pytest.raises(PipelineError, match='unknown pipeline step: zzz'): p.step('zzz')
52
-
53
- async def test_a_passing_run_walks_the_plan_and_a_failing_one_stops_it(tmp_path):
54
- """Auto-advance is what makes a release one button. It has to stop at the first failure, or a
55
- project would publish from a tree whose tests never passed."""
56
- p = mk(tmp_path, [Step('one', 'one', 'echo first'),
57
- Step('two', 'two', 'sh -c "exit 3"'),
58
- Step('three', 'three', 'echo never')])
59
- await p.start()
60
- for _ in range(200):
61
- if p.state()['failed']: break
62
- await asyncio.sleep(.05)
63
- st = p.state()
64
- assert st['failed'] == 'two'
65
- got = {r['id']: (r['status'], r['code']) for r in st['steps']}
66
- assert got['one'] == ('passed', 0) and got['two'] == ('failed', 3)
67
- assert got['three'][0] == 'pending', 'the step after a failure must not have run'
68
- assert 'first' in p.tail() and 'never' not in p.tail()
69
- assert '❯ echo first' in p.tail(), 'the transcript says which command produced what'
70
-
71
- async def test_a_viewer_that_joins_late_is_primed_with_what_it_missed(tmp_path):
72
- "A browser attaching after a step has scrolled by should still see it."
73
- p = mk(tmp_path, [Step('one', 'one', 'echo hello-from-the-past')])
74
- await p.start()
75
- for _ in range(200):
76
- if p.state()['steps'][0]['status'] == 'passed': break
77
- await asyncio.sleep(.05)
78
- q = p.subscribe()
79
- assert b'hello-from-the-past' in q.get_nowait()
80
- p.unsubscribe(q)
81
-
82
- async def test_a_second_start_is_refused_while_one_is_running(tmp_path):
83
- p = mk(tmp_path, [Step('slow', 'slow', 'sleep 5')])
84
- await p.start()
85
- with pytest.raises(PipelineError, match='is still running'): await p.start()
86
- with pytest.raises(PipelineError, match='is still running'): p.reset()
87
- p.stop()
88
- assert p.state()['steps'][0]['status'] == 'failed' and not p.state()['running']
89
-
90
- async def test_a_step_with_no_command_says_so_instead_of_spawning_a_shell(tmp_path):
91
- p = mk(tmp_path, [Step('empty', 'empty', ' ')])
92
- with pytest.raises(PipelineError, match='no command to run'): await p.start()
93
-
94
- def test_a_hyphenated_command_is_found_under_either_spelling(tmp_path):
95
- "nbdev installs `nbdev_export`; the plan spells it `nbdev-export`. Both have to resolve."
96
- bindir = tmp_path/'bin'; bindir.mkdir()
97
- (bindir/'my_tool').write_text('#!/bin/sh\n'); (bindir/'my_tool').chmod(0o755)
98
- argv = resolve_script(['my-tool', '--flag'], python=bindir/'python')
99
- assert argv[0].endswith('my_tool') and argv[1] == '--flag'
100
- assert resolve_script(['definitely-not-here'])[0] == 'definitely-not-here', 'unfound is unchanged'
101
-
102
- def test_uv_only_claims_a_project_it_actually_owns(tmp_path):
103
- assert uv_argv(tmp_path, ['pytest']) is None, 'no lock, so not uv'
@@ -1,56 +0,0 @@
1
- "What kind of project a folder holds, and the steps that follow from it."
2
- import pytest
3
- from pullup.project import (Project, Step, app_project, default_steps, fastship_project,
4
- maturin_project, nbdev_project, release_flow, rust_project)
5
-
6
- def test_an_nbdev_project_is_recognised_by_either_file_it_keeps_its_config_in(tmp_path):
7
- "nbdev 2 writes settings.ini; a migrated project writes `[tool.nbdev]` instead."
8
- assert not nbdev_project(tmp_path)
9
- (tmp_path/'settings.ini').write_text('[DEFAULT]\nlib_name = x\nnbs_path = nbs\n')
10
- assert nbdev_project(tmp_path) and release_flow(tmp_path) == 'nbdev'
11
- (tmp_path/'settings.ini').unlink()
12
- (tmp_path/'pyproject.toml').write_text('[project]\nname = "x"\n\n[tool.nbdev]\n')
13
- assert nbdev_project(tmp_path), 'a migrated project is still an nbdev project'
14
-
15
- def test_a_settings_ini_that_is_not_nbdevs_does_not_claim_the_folder(tmp_path):
16
- "`[DEFAULT]` alone is any old ini file; nbdev's has its paths in it."
17
- (tmp_path/'settings.ini').write_text('[DEFAULT]\ncolour = blue\n')
18
- assert not nbdev_project(tmp_path)
19
-
20
- def test_rust_is_asked_before_fastship_because_a_maturin_crate_has_a_pyproject_too(tmp_path):
21
- """The fastship flow would `ship-pypi` a wheel only the runners can build, so a crate that is
22
- also a Python package has to be recognised as the crate it is."""
23
- (tmp_path/'Cargo.toml').write_text('[package]\nname = "x"\n')
24
- assert rust_project(tmp_path) and release_flow(tmp_path) == 'crate'
25
- (tmp_path/'pyproject.toml').write_text('[build-system]\nbuild-backend = "maturin"\n')
26
- assert maturin_project(tmp_path) and release_flow(tmp_path) == 'maturin'
27
- assert fastship_project(tmp_path), 'it does have a pyproject; the order is what decides'
28
-
29
- def test_a_plain_folder_and_a_plain_package_get_the_flows_that_fit_them(tmp_path):
30
- assert release_flow(tmp_path) == 'plain'
31
- (tmp_path/'pyproject.toml').write_text('[project]\nname = "x"\n')
32
- assert release_flow(tmp_path) == 'fastship'
33
-
34
- def test_the_app_bundle_lands_after_the_last_publish_and_before_the_bump(tmp_path):
35
- """`nbdev_bump_version` raises the version for the next cycle, so a bundle built after it
36
- carries a version nothing was released under."""
37
- (tmp_path/'settings.ini').write_text('[DEFAULT]\nlib_name = x\nnbs_path = nbs\n')
38
- assert [s.id for s in default_steps(tmp_path)] == ['prepare', 'bump', 'gh', 'pypi']
39
- (tmp_path/'setup_app.py').write_text('')
40
- (tmp_path/'tools').mkdir(); (tmp_path/'tools'/'build_release.py').write_text('')
41
- assert app_project(tmp_path)
42
- ids = [s.id for s in default_steps(tmp_path)]
43
- assert ids == ['prepare', 'bump', 'gh', 'pypi', 'app', 'assets']
44
- assert ids.index('app') > ids.index('pypi')
45
-
46
- def test_a_project_says_what_it_is_without_the_caller_asking_five_questions(tmp_path):
47
- (tmp_path/'pyproject.toml').write_text('[project]\nname = "x"\n')
48
- p = Project(tmp_path)
49
- assert p.kind == 'fastship' and not p.packages_an_app
50
- assert [s.id for s in p.steps()] == ['test', 'changelog', 'gh', 'pypi', 'bump']
51
- assert p.dict()['kind'] == 'fastship' and p.dict()['steps'][0]['cmd'].startswith('python -m pytest')
52
-
53
- def test_a_caller_can_bring_its_own_flow(tmp_path):
54
- "`FLOWS` is data, so a project kind pullup does not ship a plan for is still reachable."
55
- mine = {'plain': [Step('only', 'the one step', 'true', 'nothing else runs')]}
56
- assert [s.id for s in default_steps(tmp_path, flows=mine)] == ['only']
@@ -1,55 +0,0 @@
1
- "Which of your own packages raised, and where to open it."
2
- from pullup.stack import blame, frames, checkout_for, survey
3
-
4
- TB = '''Traceback (most recent call last):
5
- File "/x/.venv/lib/python3.12/site-packages/nbdev/cli.py", line 10, in main
6
- run()
7
- File "/x/.venv/lib/python3.12/site-packages/gheasy/repo.py", line 42, in push
8
- raise GitError("no upstream")
9
- GitError: no upstream
10
- '''
11
-
12
- def test_the_deepest_package_of_yours_is_the_one_blamed():
13
- "The top of the traceback is whoever called; the bottom is whoever actually raised."
14
- r = blame(TB, family=['nbdev', 'gheasy'])
15
- assert r['package'] == 'gheasy' and r['line'] == 42 and r['fn'] == 'push'
16
- assert r['error'] == 'GitError: no upstream'
17
-
18
- def test_a_traceback_with_none_of_your_packages_in_it_still_reports_the_error():
19
- "A failure in somebody else's code is not a failure to describe."
20
- r = blame(TB, family=['pullup'])
21
- assert r['package'] == '' and r['error'] == 'GitError: no upstream'
22
-
23
- def test_nothing_at_all_is_answered_with_blanks_not_an_exception():
24
- assert blame('')['package'] == '' and blame(None)['error'] == ''
25
-
26
- def test_frames_reads_a_traceback_in_the_order_the_terminal_printed_it():
27
- got = frames(TB)
28
- assert [f['fn'] for f in got] == ['main', 'push'] and got[0]['line'] == 10
29
-
30
- def test_an_open_checkout_is_matched_by_what_is_inside_it_not_by_its_folder_name(tmp_path):
31
- "A repository called `gheasy-fork` still holds the `gheasy` package, and that is what counts."
32
- root = tmp_path/'gheasy-fork'; (root/'gheasy').mkdir(parents=True)
33
- (root/'gheasy'/'__init__.py').write_text('')
34
- assert checkout_for('gheasy', [tmp_path]) == ''
35
- assert checkout_for('gheasy', [root]) == str(root)
36
- assert checkout_for('nothere', [root]) == ''
37
-
38
- def test_a_src_layout_checkout_is_found_too(tmp_path):
39
- root = tmp_path/'thing'; (root/'src'/'thing').mkdir(parents=True)
40
- (root/'src'/'thing'/'__init__.py').write_text('')
41
- assert checkout_for('thing', [root]) == str(root)
42
-
43
- def test_the_blamed_file_is_reopened_in_your_checkout_when_you_have_one(tmp_path):
44
- """Site-packages is a copy. Pointing an editor at it means editing something a reinstall
45
- discards, so the checkout wins where the same file exists in it."""
46
- root = tmp_path/'gheasy'; (root/'gheasy').mkdir(parents=True)
47
- (root/'gheasy'/'__init__.py').write_text('')
48
- (root/'gheasy'/'repo.py').write_text('')
49
- r = blame(TB, checkouts=[root], family=['gheasy'])
50
- assert r['open'] == str(root/'gheasy'/'repo.py') and r['checkout'] == str(root)
51
- assert r['file'].startswith('/x/'), 'and the traceback still says where it actually ran'
52
-
53
- def test_survey_answers_for_a_package_that_is_not_installed():
54
- row = next(r for r in survey(family=['definitely_not_installed_xyz']))
55
- assert row['installed'] is False and row['source'] == '' and row['version'] == ''
@@ -1,59 +0,0 @@
1
- "Reading a GitHub Actions workflow without running one."
2
- from pullup.workflows import Workflows, levels
3
-
4
- def test_jobs_are_arranged_into_the_rows_a_graph_is_drawn_in():
5
- "Everything that can start now, then what follows it, by longest path."
6
- jobs = [{'id': 'build', 'needs': []}, {'id': 'test', 'needs': ['build']},
7
- {'id': 'lint', 'needs': []}, {'id': 'ship', 'needs': ['test', 'lint']}]
8
- assert levels(jobs) == [['build', 'lint'], ['test'], ['ship']]
9
-
10
- def test_a_job_needing_something_that_is_not_there_still_gets_a_row():
11
- "A workflow naming a job it does not define is a workflow you have to be able to look at."
12
- assert levels([{'id': 'a', 'needs': ['ghost']}]) == [['a']]
13
-
14
- def test_a_cycle_is_drawn_rather_than_hung_on():
15
- got = levels([{'id': 'a', 'needs': ['b']}, {'id': 'b', 'needs': ['a']}])
16
- assert got and sorted(sum(got, [])) == ['a', 'b']
17
-
18
- def test_a_repository_with_no_workflows_reads_as_empty_not_as_an_error(tmp_path):
19
- w = Workflows(tmp_path)
20
- assert w.files() == [] and w.parsed() == []
21
-
22
- def test_one_workflow_is_read_into_its_triggers_jobs_and_rows(tmp_path):
23
- d = tmp_path/'.github'/'workflows'; d.mkdir(parents=True)
24
- (d/'ci.yml').write_text('''name: CI
25
- on:
26
- push:
27
- branches: [main]
28
- workflow_dispatch:
29
- jobs:
30
- build:
31
- runs-on: ubuntu-latest
32
- steps:
33
- - uses: actions/checkout@v4
34
- - name: test
35
- run: pytest -q
36
- ship:
37
- needs: build
38
- runs-on: ubuntu-latest
39
- steps:
40
- - run: echo ship
41
- ''')
42
- row = Workflows(tmp_path).parse(d/'ci.yml')
43
- assert row['error'] == '' and row['name'] == 'CI'
44
- assert row['dispatchable'], 'a dispatchable workflow is one you can start from a button'
45
- assert {t['event'] for t in row['triggers']} == {'push', 'workflow_dispatch'}
46
- assert [j['id'] for j in row['jobs']] == ['build', 'ship']
47
- assert row['jobs'][1]['needs'] == ['build'] and row['levels'] == [['build'], ['ship']]
48
- assert [s['name'] for s in row['jobs'][0]['steps']] == ['actions/checkout@v4', 'test']
49
-
50
- def test_a_file_that_is_not_a_workflow_reports_why_rather_than_raising(tmp_path):
51
- d = tmp_path/'.github'/'workflows'; d.mkdir(parents=True)
52
- (d/'nope.yml').write_text('just a string\n')
53
- assert Workflows(tmp_path).parse(d/'nope.yml')['error'] == 'this file is not a workflow'
54
-
55
- def test_broken_yaml_is_an_error_on_the_row_not_an_exception(tmp_path):
56
- d = tmp_path/'.github'/'workflows'; d.mkdir(parents=True)
57
- (d/'bad.yml').write_text('name: [unclosed\n')
58
- row = Workflows(tmp_path).parse(d/'bad.yml')
59
- assert row['error'] and row['jobs'] == []
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