Skip to content

Commit

Permalink
Merge pull request #1 from kaliv0/v2
Browse files Browse the repository at this point in the history
V2
  • Loading branch information
kaliv0 authored Nov 14, 2024
2 parents 2b6636b + 228411c commit 4f363d7
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 104 deletions.
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,66 @@

---------------------------
### How to use
- Describe jobs as tables in a detox.toml file inside the root directory of your project
- Describe jobs as tables/dictionaries in a config file called 'detox' (choose between .toml, .yaml or .json format).
<br>(Put the config inside the root directory of your project)
```toml
# detox.toml
[test]
description = "test project"
dependencies = ["pytest", "pytest-cov"]
commands = "pytest -vv --disable-warnings -s --cache-clear"
```
```yaml
# detox.yaml
test:
description: test project
dependencies:
- pytest
- pytest-cov
commands:
- pytest -vv --disable-warnings -s --cache-clear
```
- <i>description</i> and <i>dependencies</i> could be optional but not <i>commands</i>
```toml
# detox.toml
[no-deps]
commands = "echo 'Hello world'"
```
(in json)
```json
"no-deps": {
"commands": "echo 'Hello world'"
}
```

- <i>dependencies</i> and <i>commands</i> could be strings or (in case of more than one) a list of strings
```toml
# detox.toml
commands = ["ruff check --fix", "ruff format --line-length=100 ."]
```
```yaml
# detox.yaml
commands:
- ruff check --fix
- ruff format --line-length=100 .
```
- You could provide a [run] table inside the toml file with a <i>'suite'</i> - list of selected jobs to run
- You could provide a [run] table inside the config file with a <i>'suite'</i> - list of selected jobs to run
```toml
[run]
suite = ["lint", "format", "test"]
```
```json
"run": {
"suite": [
"lint",
"format",
"test",
"no-deps"
]
}
```
---------------------------
- Run the tool in the terminal with a simple <b>'detox'</b> command
```shell
Expand Down Expand Up @@ -69,4 +105,4 @@ or a list of jobs
```shell
$ detox -j lint format
```
<b>NB:</b> If there is a [run] table in the toml file the jobs specified in the command line take precedence
<b>NB:</b> If there is a <i>'run'</i> table in the config file the jobs specified in the command line take precedence
2 changes: 1 addition & 1 deletion detox/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.2"
__version__ = "1.1.0"
2 changes: 1 addition & 1 deletion detox/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def get_command_line_args():
"-j",
"--jobs",
nargs="+",
help="pick a job from detox.toml file to run",
help="pick a job from config file to run",
)
return parser.parse_args()

Expand Down
174 changes: 79 additions & 95 deletions detox/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import subprocess
import sys
import time
import tomllib
from itertools import chain

from detox.logger import ToxicoLogger


class DetoxRunner:
CONFIG_FILE = "./detox.toml"
CONFIG_FILE = "detox"
CONFIG_FORMATS = [".toml", ".json", ".yaml"]
TMP_VENV = ".detoxenv"

def __init__(self):
Expand All @@ -19,16 +19,9 @@ def __init__(self):
self.successful_jobs = []
self.failed_jobs = []

self.is_toml_file_valid = False
self.is_setup_successful = False
self.is_teardown_successful = False
self.is_detox_successful = True # needed for bitwise &

@property
def skipped_jobs(self):
return [
job for job in self.all_jobs if job not in chain(self.failed_jobs, self.successful_jobs)
]
return [job for job in self.all_jobs if job not in chain(self.failed_jobs, self.successful_jobs)]

@property
def job_suite(self):
Expand All @@ -48,146 +41,137 @@ def job_suite(self):
def run(self, args):
global_start = time.perf_counter()
ToxicoLogger.info("Detoxing begins:")
self._run_detox_stages(args)
is_detox_successful = self._run_detox_stages(args)
global_stop = time.perf_counter()

if self.is_detox_successful:
if is_detox_successful:
ToxicoLogger.info(f"All jobs succeeded! {self.successful_jobs}")
ToxicoLogger.info(f"Detoxing took: {global_stop - global_start}")
else:
ToxicoLogger.fail(f"Unsuccessful detoxing took: {global_stop - global_start}")
if self.failed_jobs: # in case parsing fails before any job is run
ToxicoLogger.error(f"Failed jobs: {self.failed_jobs}")
if self.successful_jobs:
ToxicoLogger.info(
f"Successful jobs: {[x for x in self.successful_jobs if x not in self.failed_jobs]}"
)
if self.skipped_jobs:
ToxicoLogger.fail(f"Skipped jobs: {self.skipped_jobs}")

def _run_detox_stages(self, args):
self._read_file()
if not self.is_toml_file_valid:
ToxicoLogger.fail("Detoxing failed: missing or empty detox.toml file")
sys.exit(1)
return

is_valid, error_job = self._read_args(args)
if not is_valid:
ToxicoLogger.fail(f"Detoxing failed: '{error_job}' not found in detox.toml jobs")
sys.exit(1)
ToxicoLogger.fail(f"Unsuccessful detoxing took: {global_stop - global_start}")
if self.failed_jobs: # in case parsing fails before any job is run
ToxicoLogger.error(f"Failed jobs: {self.failed_jobs}")
if self.successful_jobs:
ToxicoLogger.info(
f"Successful jobs: {[x for x in self.successful_jobs if x not in self.failed_jobs]}"
)
if self.skipped_jobs:
ToxicoLogger.fail(f"Skipped jobs: {self.skipped_jobs}")

self._setup()
if not self.is_setup_successful:
ToxicoLogger.fail("Detoxing failed :(")
def _run_detox_stages(self, args):
if not (self._handle_config_file() and self._read_args(args) and self._setup()):
ToxicoLogger.fail("Detoxing failed")
sys.exit(1)

self._run_jobs()
is_detox_successful = self._run_jobs()
for _ in range(3):
self._teardown()
if self.is_teardown_successful:
is_teardown_successful = self._teardown()
if is_teardown_successful:
break

def _read_file(self):
if not os.path.exists(self.CONFIG_FILE) or os.path.getsize(self.CONFIG_FILE) == 0:
self.is_toml_file_valid = False
return

with open(self.CONFIG_FILE, "rb") as f:
self.data = tomllib.load(f)
self.is_toml_file_valid = bool(self.data)
return is_detox_successful

def _handle_config_file(self):
for config_fmt in self.CONFIG_FORMATS:
config_path = os.path.join(os.getcwd(), f"{self.CONFIG_FILE}{config_fmt}")
if not os.path.exists(config_path):
continue
if not os.path.getsize(config_path):
ToxicoLogger.fail("Empty config file")
return False
return self._read_config_file(config_path)
ToxicoLogger.fail("Config file not found")
return False

def _read_config_file(self, config_path):
with open(config_path, "rb") as f:
_, extension = os.path.splitext(config_path)
match extension:
case ".toml":
import tomllib

self.data = tomllib.load(f)
case ".json":
import json

self.data = json.load(f)
case ".yaml":
import yaml

self.data = yaml.safe_load(f)
return bool(self.data)

def _read_args(self, args):
if args.jobs is None:
return True, None
return True
for job in args.jobs:
if job not in self.data:
return False, job
ToxicoLogger.fail(f"'{job}' not found in jobs suite")
return False
self.cli_jobs = args.jobs
return True, None
return True

# setup environment
def _setup(self):
ToxicoLogger.log("Creating venv...")
prepare = f"python3 -m venv {self.TMP_VENV}"
is_successful = self._run_subprocess(prepare)
if is_successful:
self.is_setup_successful = True
else:
ToxicoLogger.error("Failed creating new virtual environment")
self.is_setup_successful = False
if self._run_subprocess(prepare):
return True
ToxicoLogger.error("Failed creating new virtual environment")
return False

def _teardown(self):
teardown = f"rm -rf {self.TMP_VENV}"
ToxicoLogger.log("Removing venv...")
is_successful = self._run_subprocess(teardown)
if is_successful:
self.is_teardown_successful = True
else:
ToxicoLogger.error("Failed removing virtual environment")
self.is_teardown_successful = False
if self._run_subprocess(teardown):
return True
ToxicoLogger.error("Failed removing virtual environment")
return False

def _run_jobs(self):
if not self.job_suite:
self.is_detox_successful = False
return
return False

is_detox_successful = True
for table, table_entries in self.job_suite:
ToxicoLogger.log("#########################################")
ToxicoLogger.start(f"{table.upper()}:")
start = time.perf_counter()

install = self._build_install_command(table_entries)
run = self._build_run_command(table, table_entries)
if not run:
self.is_detox_successful = False
return
if not (run := self._build_run_command(table, table_entries)):
return False

cmd = f"source {self.TMP_VENV}/bin/activate"
if install:
cmd += f" && {install}"
cmd += f" && {run}"

is_successful = self._run_subprocess(cmd)
if not is_successful:
if not (is_job_successful := self._run_subprocess(cmd)):
self.failed_jobs.append(table)
ToxicoLogger.error(f"{table.upper()} failed")
else:
stop = time.perf_counter()
ToxicoLogger.success(f"{table.upper()} succeeded! Took: {stop - start}")
self.successful_jobs.append(table)
self.is_detox_successful &= is_successful
is_detox_successful &= is_job_successful
ToxicoLogger.log("#########################################")
return is_detox_successful

# build shell commands
@staticmethod
def _build_install_command(table_entries):
deps = table_entries.get("dependencies", None)
if not deps:
return

install = ""
if isinstance(deps, list):
install = " ".join(deps)
else:
install += f"{deps}"
install = "pip install " + install
return install
if not (deps := table_entries.get("dependencies", None)):
return None
install = " ".join(deps) if isinstance(deps, list) else deps
return f"pip install {install}"

def _build_run_command(self, table, table_entries):
run = ""
cmds = table_entries.get("commands", None)
if not cmds:
if not (cmds := table_entries.get("commands", None)):
self.failed_jobs.append(table)
ToxicoLogger.error(
f"Encountered error: 'commands' in '{table}' table cannot be empty or missing"
)
return

if isinstance(cmds, list):
run = " && ".join(cmds)
else:
run += f"{cmds}"
return run
ToxicoLogger.error(f"Encountered error: 'commands' in '{table}' table cannot be empty or missing")
return None
return " && ".join(cmds) if isinstance(cmds, list) else cmds

# execute shell commands
@staticmethod
Expand Down
36 changes: 36 additions & 0 deletions examples/detox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"lint": {
"commands": "mypy --ignore-missing-imports .",
"dependencies": "mypy",
"description": "lint project"
},
"format": {
"commands": [
"ruff check --fix",
"ruff format --line-length=100 ."
],
"dependencies": "ruff",
"description": "format project"
},
"test": {
"commands": [
"pytest -vv --disable-warnings -s --cache-clear"
],
"dependencies": [
"pytest",
"pytest-cov"
],
"description": "test project"
},
"no-deps": {
"commands": "echo 'Hello world'"
},
"run": {
"suite": [
"lint",
"format",
"test",
"no-deps"
]
}
}
2 changes: 1 addition & 1 deletion examples/detox.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ commands = ["pytest -vv --disable-warnings -s --cache-clear"]
commands = "echo 'Hello world'"

[run]
suite = ["lint", "format", "test", "no-deps"]
suite = ["lint", "format", "test", "no-deps"]
Loading

0 comments on commit 4f363d7

Please sign in to comment.