Skip to content

Commit

Permalink
Merge pull request #32 from elcaminoreal/add_safe_ify
Browse files Browse the repository at this point in the history
Add safe ify
  • Loading branch information
moshez committed Jan 5, 2024
2 parents 073bec3 + 422aeaf commit 5f3cd8a
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 9 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/pr-main.yml
Expand Up @@ -7,12 +7,12 @@ jobs:
fail-fast: false
matrix:
include:
- { python: "3.10", os: ubuntu-latest, check: "tests-3.10" }
- { python: "3.9", os: ubuntu-latest, check: "tests-3.9" }
- { python: "3.10", os: ubuntu-latest, check: "lint" }
- { python: "3.10", os: ubuntu-latest, check: "docs" }
- { python: "3.10", os: ubuntu-latest, check: "mypy" }
- { python: "3.10", os: ubuntu-latest, check: "build" }
- { python: "3.11", os: ubuntu-latest, check: "tests-3.11" }
#- { python: "3.12", os: ubuntu-latest, check: "tests-3.12" }
- { python: "3.11", os: ubuntu-latest, check: "lint" }
- { python: "3.11", os: ubuntu-latest, check: "docs" }
- { python: "3.11", os: ubuntu-latest, check: "mypy" }
- { python: "3.11", os: ubuntu-latest, check: "build" }
name: ${{ matrix.check }} on Python ${{ matrix.python }} (${{ matrix.os }})
runs-on: ${{ matrix.os }}

Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Expand Up @@ -6,7 +6,7 @@
nox.options.envdir = "build/nox"
nox.options.sessions = ["lint", "tests", "mypy", "docs", "build"]

VERSIONS = ["3.9", "3.10"]
VERSIONS = ["3.12", "3.11"]


@nox.session(python=VERSIONS)
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Expand Up @@ -6,11 +6,12 @@ build-backend = "setuptools.build_meta"

[project]
name = "gather"
version = "2023.1.20.1"
version = "2024.1.5.1"
description = "A gatherer"
readme = "README.rst"
authors = [{name = "Moshe Zadka", email = "moshez@zadka.club"}]
dependencies = ["attrs", "incremental", "venusian"]
requires-python = ">=3.11"

[project.optional-dependencies]
tests = ["virtue", "pyhamcrest", "coverage[toml]"]
Expand All @@ -19,7 +20,7 @@ lint = ["flake8", "black", "pylint"]
docs = ["sphinx"]

[tool.coverage.run]
omit = ["build/*", "*/tests/*", "*/example/*", "**/__main__.py"]
omit = ["build/*", "*/tests/*", "*/example/*", "**/__main__.py", "**/__init__.py"]

[project.license]
text = """
Expand Down
53 changes: 53 additions & 0 deletions src/gather/commands.py
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations
import argparse
import functools
import logging
import os
import subprocess
import sys
Expand All @@ -11,6 +13,8 @@

from .api import Wrapper, unique

LOGGER = logging.getLogger(__name__)


@attrs.frozen
class _Argument:
Expand Down Expand Up @@ -85,6 +89,55 @@ def set_parser(*, collected, parser=None):
return parser


def _make_safe_run(args):
no_dry_run = getattr(args, "no_dry_run", False)
orig_run = args.orig_run

@functools.wraps(orig_run)
def wrapped_run(cmdargs, **kwargs):
real_kwargs = dict(text=True, check=True, capture_output=True)
real_kwargs.update(kwargs)
LOGGER.info("Running: %s", cmdargs)
try:
return orig_run(cmdargs, **real_kwargs)
except subprocess.CalledProcessError as exc:
exc.add_note(f"STDERR: {exc.stderr}")
exc.add_note(f"STDOUT: {exc.stdout}")
raise

@functools.wraps(orig_run)
def wrapped_dry_run(cmdargs, **kwargs):
LOGGER.info("Running: %s", cmdargs)
LOGGER.info("Dry run, skipping")

unsafe_run = wrapped_run if no_dry_run else wrapped_dry_run
args.run = unsafe_run
args.safe_run = wrapped_run
args.orig_run = orig_run


def run_maybe_dry(*, parser, argv=sys.argv, env=os.environ, sp_run=subprocess.run):
"""
Run commands that only take ``args``.
This runs commands that take ``args``.
Commands can assume that the following attributes
exist:
* ``run``: Run with logging, only if `--no-dry-run` is passed
* ``safe_run``: Run with logging
* ``orig_run``: Original function
"""
args = parser.parse_args(argv[1:])
args.orig_run = sp_run
args.env = env
_make_safe_run(args)
command = args.__gather_command__
return command(
args=args,
)


def run(*, parser, argv=sys.argv, env=os.environ, sp_run=subprocess.run):
"""
Parse arguments and run the command.
Expand Down
101 changes: 101 additions & 0 deletions src/gather/tests/test_commands.py
@@ -1,12 +1,22 @@
"""Test command dispatch"""

import argparse
import contextlib
import io
import pathlib
import os
import tempfile
import textwrap
import subprocess
import sys
import unittest
from unittest import mock
from hamcrest import (
assert_that,
all_of,
has_key,
has_entry,
not_,
string_contains_in_order,
contains_string,
calling,
Expand Down Expand Up @@ -44,6 +54,31 @@ def _do_something_else(*, args, env, run):
run([sys.executable, "-c", "print(3)"], check=True)


MAYBE_DRY_COMMANDS_COLLECTOR = gather.Collector()

MAYBE_DRY_REGISTER = commands.make_command_register(MAYBE_DRY_COMMANDS_COLLECTOR)


@MAYBE_DRY_REGISTER(
add_argument("--no-dry-run", action="store_true", default=False),
add_argument("--output-dir", required=True),
name="write-safely",
)
def _write_safely(args):
output_dir = pathlib.Path(args.output_dir)
safe = os.fspath(output_dir / "safe.txt")
unsafe = os.fspath(output_dir / "unsafe.txt")
code = textwrap.dedent(
"""\
import pathlib
import sys
pathlib.Path(sys.argv[1]).write_text(str(1 + 1))
"""
)
args.run([sys.executable, "-c", code, unsafe])
args.safe_run([sys.executable, "-c", code, safe])


class CommandTest(unittest.TestCase):

"""Test command dispatch"""
Expand Down Expand Up @@ -102,3 +137,69 @@ def test_custom_parser(self):
output,
contains_string("custom help message"),
)

def test_with_dry(self):
"""Test running command in dry-run mode"""
parser = commands.set_parser(collected=MAYBE_DRY_COMMANDS_COLLECTOR.collect())
with contextlib.ExitStack() as stack:
tmp_dir = pathlib.Path(stack.enter_context(tempfile.TemporaryDirectory()))
commands.run_maybe_dry(
parser=parser,
argv=["command", "write-safely", "--output-dir", os.fspath(tmp_dir)],
env={},
sp_run=subprocess.run,
)
contents = {child.name: child.read_text() for child in tmp_dir.iterdir()}
assert_that(
contents,
all_of(
not_(has_key("unsafe.txt")),
has_entry("safe.txt", "2"),
),
)

def test_with_no_dry(self):
"""Test running command in no dry-run mode"""
parser = commands.set_parser(collected=MAYBE_DRY_COMMANDS_COLLECTOR.collect())
with contextlib.ExitStack() as stack:
tmp_dir = pathlib.Path(stack.enter_context(tempfile.TemporaryDirectory()))
commands.run_maybe_dry(
parser=parser,
argv=[
"command",
"write-safely",
"--output-dir",
os.fspath(tmp_dir),
"--no-dry-run",
],
env={},
sp_run=subprocess.run,
)
contents = {child.name: child.read_text() for child in tmp_dir.iterdir()}
assert_that(
contents,
all_of(
has_entry("unsafe.txt", "2"),
has_entry("safe.txt", "2"),
),
)

def test_with_dry_fail(self):
"""Test running command that fails"""
parser = commands.set_parser(collected=MAYBE_DRY_COMMANDS_COLLECTOR.collect())
with contextlib.ExitStack() as stack:
tmp_dir = pathlib.Path(stack.enter_context(tempfile.TemporaryDirectory()))
assert_that(
calling(commands.run_maybe_dry).with_args(
parser=parser,
argv=[
"command",
"write-safely",
"--output-dir",
os.fspath(tmp_dir / "not-there"),
],
env={},
sp_run=subprocess.run,
),
raises(subprocess.CalledProcessError),
)

0 comments on commit 5f3cd8a

Please sign in to comment.