Metadata-Version: 2.1
Name: gird
Version: 2.1.1
Summary: Make-like build tool & task runner for Python
Home-page: https://github.com/gird-dev/gird
License: MIT
Author: Tuukka Ruhanen
Author-email: tuukka.t.ruhanen@gmail.com
Requires-Python: >=3.9,<4.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Software Development :: Build Tools
Project-URL: Repository, https://github.com/gird-dev/gird
Description-Content-Type: text/markdown

[//]: # (This README.md is autogenerated from README_template.md with the script
         render_readme.py)

[![pypi](https://img.shields.io/pypi/v/gird)](https://pypi.org/project/gird/)
![python](https://img.shields.io/pypi/pyversions/gird)
![license](https://img.shields.io/github/license/gird-dev/gird)
[![codecov](https://codecov.io/gh/gird-dev/gird/branch/master/graph/badge.svg?token=CVLPXCSHZF)](https://codecov.io/gh/gird-dev/gird)

# Gird

Gird is a lightweight & general-purpose [Make][make]-like build tool & task
runner for Python.

[make]: https://en.wikipedia.org/wiki/Make_(software)

### Features

- A simple, expressive, and intuitive rule definition and execution scheme very
  close to that of Make.
- Configuration in Python, allowing straightforward and familiar usage, without
  the need for a dedicated rule definition syntax.
- Ability to take advantage of Python's flexibility and possibility to easily
  integrate with Python libraries and tools.
- Emphasis on API simplicity & ease of use.

### Example use cases

- Data science & data analytics workflows.
- Portable CI tasks.
- Less rule-heavy application build setups. (Build time overhead may become
  noticeable with thousands of rules.)
- Any project with tasks that need to be executed automatically when some
  dependencies are updated.

## Installation

Install Gird from PyPI with `pip install gird`, or from sources with
`pip install .`.

Gird requires Python version 3.9 or newer, and is supported on Linux & macOS.

## Usage

Define rules in *girdfile.py*. Depending on the composition of a rule
definition, a rule can, for example,

- define a recipe to run a task, e.g., to update a target file,
- define prerequisites for the target, such as dependency files or other rules,
  and
- use Python functions for more complex dependency & recipe functionality.

A rule is invoked by `gird {target}`. To list rules, run `gird list`.

When invoked, a rule will be run if its target is considered outdated. This
is the case if the rule
1) has a `Phony` target,
2) has a `Path`/`TimeTracked` target that does not exist,
5) has a `Path`/`TimeTracked` target and a `Path`/`TimeTracked` dependency that is more recent than the target,
4) has an outdated `Rule`/target as a dependency, or
3) has a function dependency that returns `True`.

Rules with outdated targets are run in topological order within the
dependency graph, i.e., all outdated dependencies are updated before the
respective targets.

Python functions used as recipes need to be picklable when used in rules
defined with `parallel=True` (default). I.e., Lambda functions and locally
defined functions require `parallel=False`.



### Example rules

#### A rule with files as its target & dependency

```python
import pathlib
import gird
WHEEL = pathlib.Path("package.whl")

RULE_BUILD = gird.rule(
    target=WHEEL,
    deps=pathlib.Path("module.py"),
    recipe="python -m build --wheel",
)
```

#### A rule with a phony target

```python
RULE_TEST = gird.rule(
    target=gird.Phony("test"),
    deps=WHEEL,
    recipe="pytest",
)
```

#### A rule with other rules as dependencies

Group multiple rules together, and set the order of execution between rules.

```python
gird.rule(
    target=gird.Phony("all"),
    deps=[
        RULE_TEST,
        RULE_BUILD,
    ],
)
```

#### A rule with a Python function recipe

To parameterize a function recipe for reusability, use, e.g., `functools.partial`.

```python
import json
import functools
JSON1 = pathlib.Path("file1.json")
JSON2 = pathlib.Path("file2.json")

def create_target(json_in: pathlib.Path, json_out: pathlib.Path):
     json_out.write_text(
         json.dumps(
             json.loads(
                 json_in.read_text()
             ).update(value2="value2")
         )
     )

gird.rule(
    target=JSON2,
    deps=JSON1,
    recipe=functools.partial(create_target, JSON1, JSON2),
    parallel=True,
)
```

#### A Python function as a dependency to arbitrarily trigger rules

Below, have a remote file re-fetched if it has changed.

```python
def has_remote_changed():
    return get_checksum_local() != get_checksum_remote()

gird.rule(
    target=JSON1,
    deps=has_remote_changed,
    recipe=fetch_remote,
)
```

#### Compound recipes for mixing shell commands with Python functions

```python
gird.rule(
    target=JSON1,
    recipe=[
        "login",
        fetch_remote,
    ],
)
```

#### Implement the `TimeTracked` protocol for custom targets & dependencies

For example, define platform-specific logic to apply dependency tracking on a remote file.

```python
class RemoteFile(gird.TimeTracked):
    def __init__(self, url: str):
        self._url = url
    @property
    def id(self):
        return self._url
    @property
    def timestamp(self):
        return get_remote_file_timestamp(self._url)

gird.rule(
    target=JSON1,
    deps=RemoteFile(URL),
    recipe=fetch_remote,
)
```

#### Flexible rule definition with loops and other constructs

```python
RULES = [
    gird.rule(
        target=source.with_suffix(".json.gz"),
        deps=gird.rule(
            target=source,
            recipe=functools.partial(fetch_remote, source),
        ),
        recipe=f"gzip -k {source.resolve()}",
    )
    for source in [JSON1, JSON2]
]

```

### Example girdfile.py

This is the girdfile.py of the project itself.

```python
from itertools import chain
from pathlib import Path

from gird import Phony, rule
from scripts import assert_readme_updated, get_wheel_path, render_readme

WHEEL_PATH = get_wheel_path()

RULE_PYTEST = rule(
    target=Phony("pytest"),
    recipe="pytest -n auto --cov=gird --cov-report=xml",
    help="Run pytest & get code coverage report.",
)

RULE_MYPY = rule(
    target=Phony("mypy"),
    recipe="mypy --check-untyped-defs -p gird",
    help="Run mypy.",
)

RULE_CHECK_FORMATTING = rule(
    target=Phony("check_formatting"),
    recipe=[
        "black --check gird scripts test girdfile.py",
        "isort --check gird scripts test girdfile.py",
    ],
    help="Check formatting with Black & isort.",
)

RULE_CHECK_README_UPDATED = rule(
    target=Phony("check_readme_updated"),
    recipe=assert_readme_updated,
    help="Check that README.md is updated based on README_template.md.",
)

RULES_TEST = [
    RULE_PYTEST,
    RULE_MYPY,
    RULE_CHECK_FORMATTING,
    RULE_CHECK_README_UPDATED,
]

rule(
    target=Phony("test"),
    deps=RULES_TEST,
    help="\n".join(f"- {rule.help}" for rule in RULES_TEST),
)

rule(
    target=Path("README.md"),
    deps=chain(
        *(Path(path).iterdir() for path in ("scripts", "gird")),
        [Path("girdfile.py"), Path("pyproject.toml")],
    ),
    recipe=render_readme,
    help="Render README.md based on README_template.md.",
)

RULE_BUILD = rule(
    target=Phony("build"),
    deps=rule(
        target=WHEEL_PATH,
        recipe="poetry build --format wheel",
        listed=False,
    ),
    help="Build distribution packages for the current version.",
)
```

Respective output from `gird list`:

```
pytest
    Run pytest & get code coverage report.
mypy
    Run mypy.
check_formatting
    Check formatting with Black & isort.
check_readme_updated
    Check that README.md is updated based on README_template.md.
test
    - Run pytest & get code coverage report.
    - Run mypy.
    - Check formatting with Black & isort.
    - Check that README.md is updated based on README_template.md.
README.md
    Render README.md based on README_template.md.
build
    Build distribution packages for the current version.
```

