project

what kind of project a folder holds, and the steps that release it

Before pullup can release a project it has to know what kind of project it is, because each kind is released with different commands. This module reads the marker files in a folder (settings.ini, pyproject.toml, Cargo.toml, setup_app.py) and returns the steps that release it. Nothing here runs a command or touches the network.


source

Step

def Step(
    id:str, label:str, cmd:str, doc:str='', needs:list=<factory>, argv:list=<factory>, meta:dict=<factory>
)->None:

One command in a release or deploy, with the keys it needs and the text that describes it.

A step is one command in a release, such as uploading to PyPI. Step holds what a user interface shows about it (label and doc), what to run (cmd), and the environment keys the command needs (needs). Usually cmd is the command itself and the pipeline splits it into arguments. When a command would not split correctly, such as a python -c snippet full of quotes and newlines, put the exact arguments in argv and keep cmd as the text to display. meta carries anything else a caller wants to keep with the step.

dict returns the step as a plain dictionary, with copies of its lists so that changing the result leaves the step alone. A pipeline saves its plan from these dictionaries and reads it back, argv included:

s = Step('pypi', 'PyPI release', 'python -m twine upload dist/*',
         'Upload the built distributions to PyPI.', ['TWINE_USERNAME', 'TWINE_PASSWORD'])
s.dict()
{'id': 'pypi',
 'label': 'PyPI release',
 'cmd': 'python -m twine upload dist/*',
 'doc': 'Upload the built distributions to PyPI.',
 'needs': ['TWINE_USERNAME', 'TWINE_PASSWORD'],
 'argv': [],
 'meta': {}}

source

app_project

def app_project(
    root
):

Whether the project also builds a desktop app, marked by setup_app.py and tools/build_release.py.


source

fastship_project

def fastship_project(
    root
):

Whether root is a Python package with a pyproject.toml that is not an nbdev project.


source

maturin_project

def maturin_project(
    root
):

Whether root is a Rust crate that maturin builds into a Python wheel.


source

rust_project

def rust_project(
    root
):

Whether root is a Rust crate, meaning it has a Cargo.toml.


source

nbdev_project

def nbdev_project(
    root
):

Whether root is an nbdev project, by its nbdev 2 settings.ini or its pyproject.toml.

Each detector answers one yes-or-no question about a folder:

A pyproject.toml that is missing or is not valid TOML counts as empty instead of raising, so a half-written file never stops detection.

lib   = mkproj('a_notebook_lib', {'settings.ini': '[DEFAULT]\nlib_name = demo\nnbs_path = nbs\n'})
crate = mkproj('a_crate', {'Cargo.toml': '[package]\nname = "demo"\n'})
pkg   = mkproj('a_package', {'pyproject.toml': '[project]\nname = "demo"\n'})
[(p.name, nbdev_project(p), rust_project(p), fastship_project(p)) for p in (lib, crate, pkg)]
[('a_notebook_lib', True, False, False),
 ('a_crate', False, True, False),
 ('a_package', False, False, True)]

Each kind of project has its own list of steps. NBDEV_STEPS, FASTSHIP_STEPS, PLAIN_STEPS, MATURIN_STEPS and CRATE_STEPS are those lists, and FLOWS maps a flow name to its list. Pass your own mapping to default_steps or Project to change what a flow runs.

APP_STEPS is not a flow of its own. Its two steps build the desktop app and attach it to the GitHub release, and they are added to whichever flow the project uses. A maturin release uploads nothing from your machine: it pushes a tag, and CI builds and publishes a wheel for every platform.

These are the step ids of each flow, then the keys the fastship steps need:

{name: [s.id for s in steps] for name, steps in FLOWS.items()}
{'nbdev': ['prepare', 'bump', 'gh', 'pypi'],
 'fastship': ['test', 'changelog', 'gh', 'pypi', 'bump'],
 'plain': ['test', 'build', 'gh', 'pypi'],
 'maturin': ['test', 'wheel', 'ci', 'changelog', 'release'],
 'crate': ['test', 'clippy', 'package', 'publish']}
{s.id: s.needs for s in FASTSHIP_STEPS if s.needs}
{'changelog': ['GITHUB_TOKEN'],
 'gh': ['GITHUB_TOKEN'],
 'pypi': ['TWINE_USERNAME', 'TWINE_PASSWORD']}

source

default_steps

def default_steps(
    root, flows:NoneType=None
):

The release steps for root, with the desktop-app steps added when the project builds one.


source

release_flow

def release_flow(
    root
):

The name of the release flow that fits the project in root.

release_flow names the flow for a folder. It checks for nbdev first, then for maturin before a plain Rust crate (every maturin project is also a crate), then for fastship. Anything else, including a folder that does not exist, is plain.

default_steps returns that flow’s steps. For a project that builds a desktop app it inserts the app steps straight after the last gh or pypi step, so any step after those still runs last; a flow with neither gets them at the end.

[(p.name, release_flow(p)) for p in (lib, crate, pkg, root/'no_such_folder')]
[('a_notebook_lib', 'nbdev'),
 ('a_crate', 'crate'),
 ('a_package', 'fastship'),
 ('no_such_folder', 'plain')]
app = mkproj('a_desktop_app', {'pyproject.toml': '[project]\nname = "demo"\n',
                               'setup_app.py': '', 'tools/build_release.py': ''})
[s.id for s in default_steps(app)]
['test', 'changelog', 'gh', 'pypi', 'app', 'assets', 'bump']

The app steps land after pypi and before bump, because fastship raises the version for the next cycle only once the release is out.


source

Project

def Project(
    root, flows:NoneType=None
):

The kind of project one folder holds and the steps that release it.

Project wraps all of this for one folder. It resolves the path once, when you create it, but works out kind and packages_an_app again each time you ask, so it notices files added since. steps returns the release steps and dict returns everything as JSON-ready data for a user interface.

p = Project(pkg)
d = p.dict()
p, d['kind'], d['app'], [s['id'] for s in d['steps']]
(Project(a_package, fastship),
 'fastship',
 False,
 ['test', 'changelog', 'gh', 'pypi', 'bump'])