From a19a85d336b74af7506170378eb920468d31e1c8 Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Sun, 24 Mar 2019 22:59:17 -0400 Subject: [PATCH 1/8] initial commit --- .gitignore | 126 +++++++++ docs/api.md | 6 + docs/conf.py | 216 +++++++++++++++ docs/index.md | 56 ++++ docs/usage.md | 87 ++++++ poetry.lock | 438 +++++++++++++++++++++++++++++++ pyproject.toml | 36 +++ setup.cfg | 3 + tests/__init__.py | 0 tests/test_commands.py | 44 ++++ tests/test_parser.py | 91 +++++++ tests/yofiles/env_then_vars.yaml | 5 + tests/yofiles/simple.yaml | 8 + tests/yofiles/vars_then_env.yaml | 5 + tests/yofiles/with_env.yaml | 3 + tests/yofiles/with_vars.yaml | 3 + yo.py | 300 +++++++++++++++++++++ yo.yaml | 8 + 18 files changed, 1435 insertions(+) create mode 100644 .gitignore create mode 100644 docs/api.md create mode 100644 docs/conf.py create mode 100644 docs/index.md create mode 100644 docs/usage.md create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/test_commands.py create mode 100644 tests/test_parser.py create mode 100644 tests/yofiles/env_then_vars.yaml create mode 100644 tests/yofiles/simple.yaml create mode 100644 tests/yofiles/vars_then_env.yaml create mode 100644 tests/yofiles/with_env.yaml create mode 100644 tests/yofiles/with_vars.yaml create mode 100755 yo.py create mode 100644 yo.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..531f891 --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +# Doc Build +docs/_build + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..99c5f21 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,6 @@ +# API + +``` eval_rst +.. automodule:: yo + :members: +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..3288115 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) +import recommonmark +from recommonmark.transform import AutoStructify + + +# -- Project information ----------------------------------------------------- + +project = "yo-runner" +copyright = "2019, Oliver Sherouse" +author = "Oliver Sherouse" + +# The short X.Y version +version = "0.1" +# The full version, including alpha/beta/rc tags +release = "0.1.0.dev" + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "recommonmark", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".md" + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +default_role = "any" + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_theme_options = { + "github_user": "OliverSherouse", + "github_repo": "yo-runner", + "description": "A YAML-based task runner for lazy people", + "github_banner": True, + "show_relbars": True, +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +html_sidebars = { + "**": [ + "about.html", + "navigation.html", + "relations.html", + "searchbox.html", + "donate.html", + ] +} + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "yo-runnerdoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "yo-runner.tex", + "yo-runner Documentation", + "Oliver Sherouse", + "manual", + ) +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "yo-runner", "yo-runner Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "yo-runner", + "yo-runner Documentation", + author, + "yo-runner", + "One line description of project.", + "Miscellaneous", + ) +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] + + +# -- Extension configuration ------------------------------------------------- +def setup(app): + app.add_config_value( + "recommonmark_config", + { + # "url_resolver": lambda url: github_doc_root + url, + "auto_toc_tree_section": "Contents" + }, + True, + ) + app.add_transform(AutoStructify) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..850affb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,56 @@ +# Welcome to yo-runner's documentation\! + +Yo is a Yaml-based task runner for lazy people. When you're coding, you may +often need to run a long command many times, but you don't want to do all that +typing every time. You don't want to have to remember the options or the flags +every time. You could write a Makefile, but they're annoying. Maybe your +toolkit provides the functionality, if you want to mess with that. You could +use something like gulp or grunt, but that's a lot of overhead. All you really +want is something like directory-specific aliases. + +That's where yo comes in. All you do is write a `yo.yaml` that looks something +like this: + +``` yaml +run: poetry run flask run +serve-docs: python -m http.server --directory docs/_build +docs: + - poetry run sphinx-build docs docs/_build | tee docs/build_errors.txt + - serve-docs +test: poetry run pytest +``` + +Now, in that directory you can run `yo run` to run your app, `yo serve-docs` to +serve your documentation folder, `yo docs` to build and serve documentation, +and `yo test` to figure out why your stupid program still isn't working. And +any arguments you pass to the `yo` command will be passed through the task. + +Yo can handle single commands, sequential lists, and concurrent lists. Every +command is run on the shell, so pipes and redirects work. There's also support +for environment variables and variables internal to the `yo.yaml` so that you +don't have to type paths more than once. It's lazy all the way down. See +[Usage](usage.md) for more information on how to do all of this. + +### Similar projects + + - Sort of close to what Yo does: + - [Make](https://www.gnu.org/software/make/), if you use `.PHONY` and + generally don't worry about dependencies + - [Just](https://github.com/casey/just), probably the closest thing, aims + to be a less annoying Make + - NPM, Poetry, probably a bunch of other frameworks have something built + in that I'm too lazy to learn + - More heavy-duty: + - [Gulp](https://gulpjs.com/) + - [Grunt](https://gruntjs.com/) + +## Contents + + - [Usage](usage.md) + - [API](api.md) + +## Indices and tables + + - [genindex](genindex) + - [modindex](modindex) + - [search](search) diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..7e22d20 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,87 @@ +# Usage + +## Basics + +Yo tasks are defined in a yofile, generally called `yo.yaml`, in the directory +where the tasks will be run. The yofile should contain a yaml mapping where the +keys are the names of the tasks and the values are the tasks themselves. For +example, a `yo.yaml` with the line: + +``` .yaml +greet: python -c "print('hello, world')" +``` + +will provide a task `yo greet` that prints out "hello, world". + +You can also add additional arguments to the end of the `yo` command and they +will be passed through to the invoked task. So if your yofile contains the +line: + +``` .yaml +serve: python -m http.server +``` + +the command `yo serve 8080 --directory foo/bar` will serve the contents of +directory foo/bar on port 8080. + +Each individual task is a line of shell script, run in the system shell. This +means that yofiles may not be exactly compatible across operating systems. If +you've got complex lines of shell that need to be shared across various +systems, you may want a more sophisticated solution. If you're using Yo as a +personal convenience, go nuts. + +## Task Lists + +### Sequential Lists + +In addition to individual tasks, a yofile can define task lists. A task list +simply runs each command one at a time, with each starting after the other +finishes. Only the last item in the list will receive any additional arguments +passed to yo on the command line. A yofile with the command: + +``` .yaml +greet: + - python -c "print('hello,')" + - python -c "print('world')" +``` + +will print out "hello," on one line and "world" on the next. + +Pressing Control-C will stop the sequence, so it's not a good idea to have a +process that doesn't end on its own before the last item in the sequence. + +### Concurrent Lists + +Yofiles can also designate lists as concurrent. Concurrent lists run all tasks +at the same time. You can designate a list as concurrent by appending `_c` to +the end of its name. A yofile with the command + +``` .yaml +serve_c: + - python -m http.server + - python -m http.server 8001 --directory foo +``` + +will define a command `yo serve` that will serve the contents of the current +directory on port 8000 and the contents of the directory `foo` on port 8001 at +the same time. Control-C will end both. + +## References + +If you've defined a task, you can reference it in a later task list by name. +Simply use the name of the task as a command in the list. So a yofile with the +commands: + +``` yaml +serve-docs: python -m http.server --directory docs/_build +docs: + - poetry run sphinx-build docs docs/_build + - serve-docs +``` + +will define two commands: `yo serve-docs`, which will serve the `docs/_build` +folder, and `yo docs`, which will build the docs before serving them. + +References can be other lists. You can even reference a sequential list inside +a concurrent list, or a concurrent list inside a sequential one. Remember, +though, that a Control-C will always stop execution, so plan accordingly. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..8d68ddb --- /dev/null +++ b/poetry.lock @@ -0,0 +1,438 @@ +[[package]] +category = "dev" +description = "A configurable sidebar-enabled Sphinx theme" +name = "alabaster" +optional = false +python-versions = "*" +version = "0.7.12" + +[[package]] +category = "dev" +description = "Atomic file writes." +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.1.0" + +[[package]] +category = "dev" +description = "Internationalization utilities" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[package.dependencies] +pytz = ">=0a" + +[[package]] +category = "dev" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.3.9" + +[[package]] +category = "dev" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.4.1" + +[[package]] +category = "dev" +description = "Python parser for the CommonMark Markdown spec" +name = "commonmark" +optional = false +python-versions = "*" +version = "0.8.1" + +[package.dependencies] +future = "*" + +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = "*" +version = "0.14" + +[[package]] +category = "dev" +description = "Discover and load entry points from installed packages." +name = "entrypoints" +optional = false +python-versions = ">=2.7" +version = "0.3" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8, pyflakes and co" +name = "flake8" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.7.7" + +[package.dependencies] +entrypoints = ">=0.3.0,<0.4.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.5.0,<2.6.0" +pyflakes = ">=2.1.0,<2.2.0" + +[package.dependencies.typing] +python = "<3.5" +version = "*" + +[[package]] +category = "dev" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.17.1" + +[[package]] +category = "dev" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "dev" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +name = "imagesize" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "dev" +description = "A small but fast and easy to use stand-alone template engine written in pure python." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[[package]] +category = "dev" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.4" +version = "6.0.0" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.0" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Object-oriented filesystem paths" +marker = "python_version < \"3.6\"" +name = "pathlib2" +optional = false +python-versions = "*" +version = "2.3.3" + +[package.dependencies] +six = "*" + +[package.dependencies.scandir] +python = "<3.5" +version = "*" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.9.0" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.5.0" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.1" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = "*" +version = "2.3.1" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = "*" +version = "2.3.1" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.10.1" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +pluggy = ">=0.7" +py = ">=1.5.0" +setuptools = "*" +six = ">=1.10.0" + +[package.dependencies.pathlib2] +python = "<3.6" +version = ">=2.2.0" + +[[package]] +category = "dev" +description = "pytest plugin to check FLAKE8 requirements" +name = "pytest-flake8" +optional = false +python-versions = "*" +version = "1.0.4" + +[package.dependencies] +flake8 = ">=3.5" +pytest = ">=3.5" + +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2018.9" + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = "*" +version = "5.1" + +[[package]] +category = "dev" +description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." +name = "recommonmark" +optional = false +python-versions = "*" +version = "0.5.0" + +[package.dependencies] +commonmark = ">=0.7.3" +docutils = ">=0.11" +sphinx = ">=1.3.1" + +[[package]] +category = "dev" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.21.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25" + +[[package]] +category = "dev" +description = "scandir, a better directory iterator and faster os.walk()" +marker = "python_version < \"3.5\"" +name = "scandir" +optional = false +python-versions = "*" +version = "1.10.0" + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.12.0" + +[[package]] +category = "dev" +description = "This package provides 16 stemmer algorithms (15 + Poerter English stemmer) generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "1.2.1" + +[[package]] +category = "dev" +description = "Python documentation generator" +name = "sphinx" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.5" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3,<2.0 || >2.0" +colorama = ">=0.3.5" +docutils = ">=0.11" +imagesize = "*" +packaging = "*" +requests = ">=2.0.0" +setuptools = "*" +six = ">=1.5" +snowballstemmer = ">=1.1" +sphinxcontrib-websupport = "*" + +[package.dependencies.typing] +python = "<3.5" +version = "*" + +[[package]] +category = "dev" +description = "Sphinx API for Web Apps" +name = "sphinxcontrib-websupport" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "dev" +description = "Type Hints for Python" +marker = "python_version < \"3.5\"" +name = "typing" +optional = false +python-versions = "*" +version = "3.6.6" + +[[package]] +category = "dev" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +version = "1.24.1" + +[metadata] +content-hash = "2252baa3d98c5b5f0a0b851efb0e16f4daadad362dd693b8c764be89cbadf3ec" +python-versions = "^3.4" + +[metadata.hashes] +alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"] +atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] +attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] +babel = ["6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", "8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"] +certifi = ["59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"] +chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +commonmark = ["9f6dda7876b2bb88dd784440166f4bc8e56cb2b2551264051123bacb0b6c1d8a", "abcbc854e0eae5deaf52ae5e328501b78b4a0758bf98ac8bb792fce993006084"] +docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] +entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] +flake8 = ["859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", "a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"] +future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] +idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] +imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"] +jinja2 = ["74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", "f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"] +markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] +mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] +more-itertools = ["0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", "590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"] +packaging = ["0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", "9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"] +pathlib2 = ["25199318e8cc3c25dcb45cbe084cc061051336d5a9ea2a12448d3d8cb748f742", "5887121d7f7df3603bca2f710e7219f3eca0eb69e0b7cc6e0a022e155ac931a7"] +pluggy = ["19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", "84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"] +py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] +pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] +pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] +pygments = ["5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", "e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"] +pyparsing = ["66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", "f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"] +pytest = ["3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", "e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"] +pytest-flake8 = ["4d225c13e787471502ff94409dcf6f7927049b2ec251c63b764a4b17447b60c0", "d7e2b6b274a255b7ae35e9224c85294b471a83b76ecb6bd53c337ae977a499af"] +pytz = ["32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", "d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"] +pyyaml = ["1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", "436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", "460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", "5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", "7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", "9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", "a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", "aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", "c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", "c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", "e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19"] +recommonmark = ["a520b8d25071a51ae23a27cf6252f2fe387f51bdc913390d83b2b50617f5bb48", "c85228b9b7aea7157662520e74b4e8791c5eacd375332ec68381b52bf10165be"] +requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"] +scandir = ["2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e", "2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022", "2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f", "2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f", "4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae", "67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173", "7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4", "8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32", "92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188", "b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d", "cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"] +six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] +snowballstemmer = ["919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", "9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"] +sphinx = ["9f3e17c64b34afc653d7c5ec95766e03043cc6d80b0de224f59b6b6e19d37c3c", "c7658aab75c920288a8cf6f09f244c6cfdae30d82d803ac1634d9f223a80ca08"] +sphinxcontrib-websupport = ["68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", "9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"] +typing = ["4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", "57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", "a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a"] +urllib3 = ["61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..408b886 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "yo-runner" +version = "0.1.0.dev" +description = "A YAML-driven task runner for lazy people" +authors = ["Oliver Sherouse "] +license = "GPL-2.0+" +readme = "README.md" +homepage = "https://github.com/OliverSherouse/yo-runner" +repository = "https://github.com/OliverSherouse/yo-runner" +keywords = ["build", "task", "runner"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Utilities", + "Topic :: Software Development :: Build Tools" +] +packages = [{include = "yo.py"}] + +[tool.poetry.dependencies] +python = "^3.4" +pyyaml = "^5.1" + +[tool.poetry.dev-dependencies] +pytest = "^3.0" +pytest-flake8 = "^1.0" +Sphinx = "^1.8" +recommonmark = "^0.5.0" + +[tool.poetry.scripts] +yo = "yo:main" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..56dcadd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[tool:pytest] +addopts = --flake8 + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..b89d596 --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,44 @@ +import subprocess +import time +import urllib.request +import urllib.error + +import pytest + +from pathlib import Path + +yofile_dir = Path(__file__).parent.joinpath("yofiles") + + +def check_server(server): + try: + urllib.request.urlopen(server) + return True + except urllib.error.URLError: + return False + + +def test_simple(): + result = subprocess.check_output( + ["yo", "-f", yofile_dir.joinpath("simple.yaml"), "foo"] + ).decode() + assert result == "hello, world\n" + + +def test_multiple(): + result = subprocess.check_output( + ["yo", "-f", yofile_dir.joinpath("simple.yaml"), "bar"] + ).decode() + assert result == "hi\nthere\n" + + +def test_concurrent(): + task = subprocess.Popen( + ["yo", "-f", yofile_dir.joinpath("simple.yaml"), "baz"] + ) + time.sleep(2) + try: + assert check_server("http://localhost:8555") + assert check_server("http://localhost:8556") + finally: + task.terminate() diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..10d82d0 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,91 @@ +import os + +import pytest +import yo + +from pathlib import Path + +HERE = Path(__file__).parent +YOFILE_DIR = HERE.joinpath("yofiles") + + +@pytest.fixture +def simplefile(): + yield yo.TaskDefs(YOFILE_DIR.joinpath("simple.yaml")) + + +def test_single(simplefile): + assert simplefile["foo"].cmd == "python -c \"print('hello, world')\"" + + +def test_multiple(simplefile): + assert isinstance(simplefile["bar"], yo.SequentialTaskList) + + +def test_multiple_cmd(simplefile): + assert simplefile["bar"].tasks[0].cmd == ("python -c \"print('hi')\"") + assert simplefile["bar"].tasks[1].cmd == ("python -c \"print('there')\"") + + +def test_concurrent(simplefile): + assert isinstance(simplefile["baz"], yo.ConcurrentTaskList) + + +def test_reference(simplefile): + assert simplefile["baz"].tasks[-1].tasks[0].cmd == ( + "python -c \"print('hi')\"" + ) + + +@pytest.fixture +def envfile(): + yield yo.TaskDefs(YOFILE_DIR.joinpath("with_env.yaml")) + + +def test_env(envfile): + assert envfile.env["foo"] == "bar" + + +def test_task_after_env(envfile): + assert envfile["print"].cmd == ( + "python -c \"import os; print(os.environ['foo'])\"" + ) + + +@pytest.fixture +def varfile(): + yield yo.TaskDefs(YOFILE_DIR.joinpath("with_vars.yaml")) + + +def test_task_after_var(varfile): + assert varfile["print"].cmd == 'python -c "print(bar)"' + + +@pytest.fixture +def env_then_vars(): + yield yo.TaskDefs(YOFILE_DIR.joinpath("env_then_vars.yaml")) + + +def test_env_then_vars_environ(env_then_vars): + assert env_then_vars.env["greeting"] == "hello" + + +def test_env_then_vars_task(env_then_vars): + assert env_then_vars["print"].cmd == ( + 'python -c "import os; ' "print(os.envirion['greeting'] + ' world')\"" + ) + + +@pytest.fixture +def vars_then_env(): + yield yo.TaskDefs(YOFILE_DIR.joinpath("vars_then_env.yaml")) + + +def test_vars_then_env_environ(vars_then_env): + assert vars_then_env.env["myname"] == "world" + + +def test_vars_then_env_task(vars_then_env): + assert vars_then_env["print"].cmd == ( + 'python -c "import os; ' "print('hello ' + os.environ['myname'])\"" + ) diff --git a/tests/yofiles/env_then_vars.yaml b/tests/yofiles/env_then_vars.yaml new file mode 100644 index 0000000..b4e0cfe --- /dev/null +++ b/tests/yofiles/env_then_vars.yaml @@ -0,0 +1,5 @@ +env: + greeting: hello +vars: + myname: world +print: python -c "import os; print(os.envirion['greeting'] + ' {myname}')" diff --git a/tests/yofiles/simple.yaml b/tests/yofiles/simple.yaml new file mode 100644 index 0000000..21f623f --- /dev/null +++ b/tests/yofiles/simple.yaml @@ -0,0 +1,8 @@ +foo: python -c "print('hello, world')" +bar: + - python -c "print('hi')" + - python -c "print('there')" +baz_c: + - python -m http.server 8555 + - python -m http.server 8556 + - bar diff --git a/tests/yofiles/vars_then_env.yaml b/tests/yofiles/vars_then_env.yaml new file mode 100644 index 0000000..e487b1f --- /dev/null +++ b/tests/yofiles/vars_then_env.yaml @@ -0,0 +1,5 @@ +vars: + greeting: hello +env: + myname: world +print: python -c "import os; print('{greeting} ' + os.environ['myname'])" diff --git a/tests/yofiles/with_env.yaml b/tests/yofiles/with_env.yaml new file mode 100644 index 0000000..78b8a1a --- /dev/null +++ b/tests/yofiles/with_env.yaml @@ -0,0 +1,3 @@ +env: + foo: bar +print: python -c "import os; print(os.environ['foo'])" diff --git a/tests/yofiles/with_vars.yaml b/tests/yofiles/with_vars.yaml new file mode 100644 index 0000000..a65017a --- /dev/null +++ b/tests/yofiles/with_vars.yaml @@ -0,0 +1,3 @@ +vars: + foo: bar +print: python -c "print({foo})" diff --git a/yo.py b/yo.py new file mode 100755 index 0000000..ac3f1f0 --- /dev/null +++ b/yo.py @@ -0,0 +1,300 @@ +#! /usr/bin/env python3 +""" +Yo - a yaml-driven task runner for lazy people +""" + +import argparse +import asyncio +import collections +import functools +import logging +import os +import shlex +import signal +import sys + +import yaml + +from pathlib import Path + +log = logging.getLogger(name="yo") +__version__ = "0.1.0.dev" + + +def error(message): + """ Give error message and exit""" + log.error(message) + sys.exit(1) + + +def _extract_env_and_vars(parsed): + """ + Remove and appropriately process "vars" and "env" sections of parsed + taskfile if present in the first first two sections. Modifies + **parsed** in place + + If an "env" section exists, add the variables defined in it to the + environment. If a "vars" section exist, extract them. + + Arguments: + + * **parsed**: a parsed representation of a taskfile + + Returns: a dictionary containing vars defined in **parsed** + + """ + # There has *got* to be a way to do this more simply + env = os.environ.copy() + vars = {} + first = tuple(parsed.keys())[0] + if first in {"env", "vars"}: + if first == "env": + env.update(parsed.pop("env")) + else: + vars = parsed.pop("vars") + second = tuple(parsed.keys())[0] + if second in {"env", "vars"}: + if second == "env": + env.update(parsed.pop("env")) + else: + vars = parsed.pop("vars") + return env, vars + + +class TaskDefs(dict): + """ + A dict-like mapping of command names to `Task` objects + + Arguments: + + * **taskfile**: a Path leading to a taskfile + """ + + def __init__(self, taskfile): + self.taskfile = Path(taskfile) + parsed = yaml.load(self.taskfile.open(), Loader=yaml.SafeLoader) + self.env, self.vars = _extract_env_and_vars(parsed) + self.tasks = self._parse_tasks(parsed) + + def __str__(self): + """Print available tasks and exit""" + to_str = ["Available tasks:"] + to_str.extend(["\t{}".format(task) for task in self.tasks]) + return "\n".join(to_str) + + def __getitem__(self, key): + return self.tasks[key] + + def _parse_tasks(self, parsed): + tasks = {} + for taskname, body in parsed.items(): + logging.debug("Parsing `{}`".format(body)) + if isinstance(body, str): + tasks[taskname] = Task(body.format(**self.vars), self.env) + else: + subtasks = [] + for subspec in body: + try: + subtasks.append(tasks[subspec]) + except KeyError: + subtasks.append( + Task(subspec.format(**self.vars), self.env) + ) + if taskname.endswith("_c"): + tasks[taskname[:-2]] = ConcurrentTaskList(subtasks) + else: + tasks[taskname] = SequentialTaskList(subtasks) + return tasks + + +class Task(object): + """ + An individual task with base command *cmd*. + + Arguments: + + * cmd: the base command associated with this task + * env: a dictionary containing environment variables and their values or + None + """ + + def __init__(self, cmd, env=None): + logging.debug('Creating task with command "{}"'.format(cmd)) + self.cmd = cmd + self.proc = None + self.env = env or {} + + def terminate(self): + """Terminate the task's process, if running""" + try: + if self.proc.returncode is None: + log.debug(f"Terminating {self.cmd}") + self.proc.terminate() + except AttributeError: + pass + + async def run(self, args=None): + """ + Run the command, with additional args if given + + Arguments: + + * args: if specified, a sequence of tokens to be appended to the base + command + """ + cmd = self.cmd + if args: + cmd = " ".join(cmd, " ".join(shlex.quote(token) for token in args)) + log.info(cmd) + self.proc = await asyncio.create_subprocess_shell(cmd, env=self.env) + await self.proc.wait() + + +class TaskList(object): + """ + Base class for Task Lists + + Arguments: + + * tasks: a sequence of :class:`Task` objecs + """ + + def __init__(self, tasks, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tasks = tasks + + +class SequentialTaskList(TaskList): + """ + A sequence of tasks of tasks to be run sequentially + + Arguments: + + * tasks: a sequence of :class:`Task` objecs + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.kill = False + self.current = None + + async def run(self, args=None): + last = len(self.tasks) - 1 + for i, task in enumerate(self.tasks): + if self.kill: + break + self.current = task + await task.run(args if i == last else None) + + def terminate(self): + self.kill = True + try: + self.current.terminate() + except AttributeError: + pass + + +class ConcurrentTaskList(TaskList): + """ + A sequence of tasks of tasks to be run concurrently + + Arguments: + + * tasks: a sequence of :class:`Task` objecs + """ + + async def run(self, args=None): + if args: + error("Additional arguments not valid for concurrent tasks") + await asyncio.wait( + [task.run() for task in self.tasks], + return_when=asyncio.FIRST_EXCEPTION, + ) + + def terminate(self): + for task in self.tasks: + task.terminate() + + +def parse_args(): + """ Parse cli arguments """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "-f", + "--taskfile", + help="file containing task definitions", + type=Path, + default=Path("yo.yaml"), + ) + parser.add_argument( + "--list", + help="list available tasks", + action="store_true", + default=False, + ) + verbose = parser.add_mutually_exclusive_group() + verbose.add_argument( + "-v", + "--verbose", + help="Print additional information", + action="store_const", + const=logging.DEBUG, + default=logging.INFO, + ) + verbose.add_argument( + "-q", + "--quiet", + dest="verbose", + help="Print less information", + action="store_const", + const=logging.WARNING, + ) + parser.add_argument("task", help="Name of task to execute", nargs="?") + parser.add_argument( + "task_args", + help="Additional arguments to pass to task command", + nargs="*", + ) + args = parser.parse_intermixed_args() + if not (args.list or args.task): + parser.error("Must specify a task or --list") + return args + + +def handle_signal(signum, task): + """On any signal, terminate the active task""" + log.debug(f"Caught {signal.Signals(signum).name}") + task.terminate() + + +def main(): + args = parse_args() + logging.basicConfig(level=args.verbose) + tasks = TaskDefs(args.taskfile) + if args.list: + print(tasks) + sys.exit() + try: + task = tasks[args.task] + except KeyError: + error("Task {} not defined. {}".format(args.task, tasks)) + loop = asyncio.get_event_loop() + for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT): + handler = functools.partial(handle_signal, sig, task) + loop.add_signal_handler(sig, handler) + loop.run_until_complete(task.run()) + + +def dict_constructor(loader, node): + """A constructor using OrderedDict for mappings""" + return collections.OrderedDict(loader.construct_pairs(node)) + + +# Add our constructor so that Tasks are in order +yaml.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, dict_constructor +) + + +if __name__ == "__main__": + main() diff --git a/yo.yaml b/yo.yaml new file mode 100644 index 0000000..a3c42da --- /dev/null +++ b/yo.yaml @@ -0,0 +1,8 @@ +vars: + docdir: docs + docbuilddir: docs/_build +build-docs: sphinx-build {docdir} {docbuilddir} +test: pytest +docs: + - build-docs + - python -m http.server --directory {docbuilddir} From 87bcc900420a51b39034173356d43357a7a5db6e Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Sun, 24 Mar 2019 23:06:22 -0400 Subject: [PATCH 2/8] travis --- .travis.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .travis.yaml diff --git a/.travis.yaml b/.travis.yaml new file mode 100644 index 0000000..d908973 --- /dev/null +++ b/.travis.yaml @@ -0,0 +1,9 @@ +language: python +python: + - "3.5" + - "3.6" + - "3.7" +install: + - curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python + - poetry install +script: poetry run pytest From 4d05db0b6fde1dc1f58632930bbabb8622096142 Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Sun, 24 Mar 2019 23:11:46 -0400 Subject: [PATCH 3/8] renamed travis file --- .travis.yaml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .travis.yaml => .travis.yml (100%) diff --git a/.travis.yaml b/.travis.yml similarity index 100% rename from .travis.yaml rename to .travis.yml From 50040ffbb883d9ecf8ef4d88fb2d2767da4a9d17 Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Sun, 24 Mar 2019 23:15:19 -0400 Subject: [PATCH 4/8] added env source --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d908973..25c5f11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,6 @@ python: - "3.7" install: - curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python + - source $HOME/.poetry/env - poetry install script: poetry run pytest From fb29a6d9ca43ebae616858ef7866289ddb092705 Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Sun, 24 Mar 2019 23:20:51 -0400 Subject: [PATCH 5/8] 3.7+, apparently --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 25c5f11..c3d0c4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python +dist: xenial python: - - "3.5" - - "3.6" - "3.7" install: - curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python From 80efb7aaa413ff030a4d18261df268df1d50bac7 Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Sun, 24 Mar 2019 23:37:06 -0400 Subject: [PATCH 6/8] flake8 --- docs/conf.py | 1 - tests/test_commands.py | 2 -- tests/test_parser.py | 2 -- 3 files changed, 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3288115..c6b6fc2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,6 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) -import recommonmark from recommonmark.transform import AutoStructify diff --git a/tests/test_commands.py b/tests/test_commands.py index b89d596..f5d92b4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -3,8 +3,6 @@ import urllib.request import urllib.error -import pytest - from pathlib import Path yofile_dir = Path(__file__).parent.joinpath("yofiles") diff --git a/tests/test_parser.py b/tests/test_parser.py index 10d82d0..a91621f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,3 @@ -import os - import pytest import yo From 653a6e7e34f2c51b2be337444b43458932c2b726 Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Mon, 25 Mar 2019 21:52:12 -0400 Subject: [PATCH 7/8] travis --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.travis.yml b/.travis.yml index c3d0c4c..3773252 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,18 @@ install: - source $HOME/.poetry/env - poetry install script: poetry run pytest +after_success: + - test $TRAVIS_BRANCH = "master" && test $TRAVIS_PULL_REQUEST = "false" && poetry run sphinx-build docs docs/_build +deploy: + - provider: script + script: poetry publish --build -u $PYPI_USER -p $PYPI_PASSWORD + on: + branch: master + tags: true + - provider: pages + github-token: $GITHUB_TOKEN + committer-from-gh: true + skip-cleanup: true + local-dir: docs/_build + on: + branch: master From 987e638065ef94a46e5bc453e2bb216220ad616c Mon Sep 17 00:00:00 2001 From: Oliver Sherouse Date: Mon, 25 Mar 2019 22:13:54 -0400 Subject: [PATCH 8/8] actually filled in the README --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index e69de29..b49f033 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,34 @@ +# Yo-Runner + +Yo is a Yaml-based task runner for lazy people. When you're coding, you may +often need to run a long command many times, but you don't want to do all that +typing every time. You don't want to have to remember the options or the flags +every time. You could write a Makefile, but they're annoying. Maybe your +toolkit provides the functionality, if you want to mess with that. You could +use something like gulp or grunt, but that's a lot of overhead. All you really +want is something like directory-specific aliases. + +That's where yo comes in. All you do is write a `yo.yaml` that looks something +like this: + +``` {.yaml} +run: poetry run flask run +serve-docs: python -m http.server --directory docs/_build +docs: + - poetry run sphinx-build docs docs/_build | tee docs/build_errors.txt + - serve-docs +test: poetry run pytest +``` + +Now, in that directory you can run `yo run` to run your app, `yo serve-docs` to +serve your documentation folder, `yo docs` to build and serve documentation, +and `yo test` to figure out why your stupid program still isn't working. And +any arguments you pass to the `yo` command will be passed through the task. + +Yo can handle single commands, sequential lists, and concurrent lists. Every +command is run on the shell, so pipes and redirects work. There's also support +for environment variables and variables internal to the `yo.yaml` so that you +don't have to type paths more than once. It's lazy all the way down. + +See the full documentation for yo at +.