Skip to content

Commit

Permalink
Added lswap and lreverse commands (#300)
Browse files Browse the repository at this point in the history
Also:
- Improved command testing stuff
- Updated dependencies (changed to >= for dev deps)
  • Loading branch information
abey79 authored Jul 9, 2021
1 parent e5e1eca commit ed26fb6
Show file tree
Hide file tree
Showing 8 changed files with 475 additions and 316 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
#### 1.8 (UNRELEASED)

New features and improvements:
* ...
* Added `lswap` command to swap the content of two layers (#300)
* Added `lreverse` command to reverse the order of paths within a layer (#300)

Bug fixes:
* ...
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ and much more.
#### General

- Easy to use **CLI** interface with integrated help (`vpype --help`and `vpype COMMAND --help`) and support for arbitrary units (e.g. `vpype read input.svg translate 3cm 2in`).
- First-class **multi-layer support** with global or per-layer processing (e.g. `vpype COMMANDNAME --layer 1,3`) and optionally-probabilistic layer edition commands ([`lmove`](https://vpype.readthedocs.io/en/stable/reference.html#lmove), [`lcopy`](https://vpype.readthedocs.io/en/stable/reference.html#lcopy), [`ldelete`](https://vpype.readthedocs.io/en/stable/reference.html#ldelete)).
- First-class **multi-layer support** with global or per-layer processing (e.g. `vpype COMMANDNAME --layer 1,3`) and optionally-probabilistic layer edition commands ([`lmove`](https://vpype.readthedocs.io/en/stable/reference.html#lmove), [`lcopy`](https://vpype.readthedocs.io/en/stable/reference.html#lcopy), [`ldelete`](https://vpype.readthedocs.io/en/stable/reference.html#ldelete), [`lswap`](https://vpype.readthedocs.io/en/stable/reference.html#lswap), [`lreverse`](https://vpype.readthedocs.io/en/stable/reference.html#lreverse)).
- Powerful hardware-accelerated **display** command with adjustable units, optional per-line coloring, optional pen-up trajectories display and per-layer visibility control ([`show`](https://vpype.readthedocs.io/en/stable/reference.html#show)).
- Geometry **statistics** extraction ([`stat`](https://vpype.readthedocs.io/en/stable/reference.html#stat)).
- Support for **command history** recording (`vpype -H [...]`)
Expand Down
425 changes: 218 additions & 207 deletions poetry.lock

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,19 @@ sphinx-rtd-theme = { version = "^0.5.0", optional = true }
recommonmark = { version = ">=0.6,<0.8", optional = true }

[tool.poetry.dev-dependencies]
coverage = {extras = ["toml"], version = "^5.4"}
pytest = "^6.2.3"
pytest-cov = "^2.11.0"
pytest-benchmark = "^3.2.3"
black = "^21.5b1"
isort = "^5.8.0"
coverage = {extras = ["toml"], version = ">=5.4"}
pytest = ">=6.2.3"
pytest-cov = ">=2.11.0"
pytest-benchmark = ">=3.2.3"
black = ">=21.5b2"
isort = ">=5.8.0"
pyinstaller = "^4.3"
packaging = "^20.8"
pytest-mpl = "^0.12"
mypy = "^0.901"
types-cachetools = "^0.1.6"
types-toml = "^0.1.1"
types-pkg-resources = "^0.1.2"
packaging = ">=20.8"
pytest-mpl = ">=0.12"
mypy = ">=0.901"
types-cachetools = ">=0.1.6"
types-toml = ">=0.1.1"
types-pkg-resources = ">=0.1.2"

[tool.poetry.extras]
docs = ["Sphinx", "sphinx-click", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "recommonmark"]
Expand Down
148 changes: 84 additions & 64 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import itertools
from dataclasses import dataclass

import numpy as np
import pytest
Expand All @@ -12,45 +13,58 @@

EXAMPLE_SVG = TESTS_DIRECTORY / "data" / "test_svg" / "svg_width_height" / "percent_size.svg"


@dataclass
class Command:
command: str
exit_code_no_layer: int
exit_code_one_layer: int
exit_code_two_layers: int


MINIMAL_COMMANDS = [
"begin grid 2 2 line 0 0 10 10 end",
"begin repeat 2 line 0 0 10 10 end",
"frame",
"random",
"line 0 0 1 1",
"rect 0 0 1 1",
"arc 0 0 1 1 0 90",
"circle 0 0 1",
"ellipse 0 0 2 4",
f"read '{EXAMPLE_SVG}'",
f"read -m '{EXAMPLE_SVG}'",
"write -f svg -",
"write -f hpgl -d hp7475a -p a4 -",
"rotate 0",
"scale 1 1",
"scaleto 10cm 10cm",
"skew 0 0",
"translate 0 0",
"crop 0 0 1 1",
"linesort",
"linesort --two-opt",
"linemerge",
"linesimplify",
"multipass",
"reloop",
"lmove 1 new",
"lcopy 1 new",
"ldelete 1",
"trim 1mm 1mm",
"splitall",
"filter --min-length 1mm",
"pagesize 10inx15in",
"stat",
"snap 1",
"reverse",
"layout a4",
"squiggles",
"text 'hello wold'",
Command("begin grid 2 2 line 0 0 10 10 end", 0, 0, 0),
Command("begin repeat 2 line 0 0 10 10 end", 0, 0, 0),
Command("frame", 0, 0, 0),
Command("random", 0, 0, 0),
Command("line 0 0 1 1", 0, 0, 0),
Command("rect 0 0 1 1", 0, 0, 0),
Command("arc 0 0 1 1 0 90", 0, 0, 0),
Command("circle 0 0 1", 0, 0, 0),
Command("ellipse 0 0 2 4", 0, 0, 0),
Command(f"read '{EXAMPLE_SVG}'", 0, 0, 0),
Command(f"read -m '{EXAMPLE_SVG}'", 0, 0, 0),
Command("write -f svg -", 0, 0, 0),
Command("write -f hpgl -d hp7475a -p a4 -", 0, 0, 0),
Command("rotate 0", 0, 0, 0),
Command("scale 1 1", 0, 0, 0),
Command("scaleto 10cm 10cm", 0, 0, 0),
Command("skew 0 0", 0, 0, 0),
Command("translate 0 0", 0, 0, 0),
Command("crop 0 0 1 1", 0, 0, 0),
Command("linesort", 0, 0, 0),
Command("linesort --two-opt", 0, 0, 0),
Command("linemerge", 0, 0, 0),
Command("linesimplify", 0, 0, 0),
Command("multipass", 0, 0, 0),
Command("reloop", 0, 0, 0),
Command("lmove 1 new", 0, 0, 0),
Command("lcopy 1 new", 0, 0, 0),
Command("ldelete 1", 0, 0, 0),
Command("lswap 1 2", 2, 2, 0),
Command("lreverse 1", 0, 0, 0),
Command("line 0 0 10 10 lreverse 1", 0, 0, 0),
Command("random -l1 random -l2 lswap 1 2", 0, 0, 0),
Command("trim 1mm 1mm", 0, 0, 0),
Command("splitall", 0, 0, 0),
Command("filter --min-length 1mm", 0, 0, 0),
Command("pagesize 10inx15in", 0, 0, 0),
Command("stat", 0, 0, 0),
Command("snap 1", 0, 0, 0),
Command("reverse", 0, 0, 0),
Command("layout a4", 0, 0, 0),
Command("squiggles", 0, 0, 0),
Command("text 'hello wold'", 0, 0, 0),
]

# noinspection SpellCheckingInspection
Expand All @@ -61,50 +75,54 @@
)


@pytest.mark.parametrize("args", MINIMAL_COMMANDS)
def test_commands_empty_geometry(runner, args):
result = runner.invoke(cli, args, catch_exceptions=False)
assert result.exit_code == 0
@pytest.mark.parametrize("cmd", MINIMAL_COMMANDS)
def test_commands_empty_geometry(runner, cmd):
result = runner.invoke(cli, cmd.command, catch_exceptions=False)
assert result.exit_code == cmd.exit_code_no_layer


@pytest.mark.parametrize("args", MINIMAL_COMMANDS)
def test_commands_single_line(runner, args):
result = runner.invoke(cli, "line 0 0 10 10 " + args, catch_exceptions=False)
assert result.exit_code == 0
@pytest.mark.parametrize("cmd", MINIMAL_COMMANDS)
def test_commands_single_line(runner, cmd):
result = runner.invoke(cli, "line 0 0 10 10 " + cmd.command, catch_exceptions=False)
assert result.exit_code == cmd.exit_code_one_layer


@pytest.mark.parametrize("args", MINIMAL_COMMANDS)
def test_commands_degenerate_line(runner, args):
result = runner.invoke(cli, "line 0 0 0 0 " + args)
assert result.exit_code == 0
@pytest.mark.parametrize("cmd", MINIMAL_COMMANDS)
def test_commands_degenerate_line(runner, cmd):
result = runner.invoke(cli, "line 0 0 0 0 " + cmd.command)
assert result.exit_code == cmd.exit_code_one_layer


@pytest.mark.parametrize("args", MINIMAL_COMMANDS)
def test_commands_random_input(runner, args):
result = runner.invoke(cli, "random -n 100 " + args)
assert result.exit_code == 0
@pytest.mark.parametrize("cmd", MINIMAL_COMMANDS)
def test_commands_random_input(runner, cmd):
result = runner.invoke(cli, "random -n 100 " + cmd.command)
assert result.exit_code == cmd.exit_code_one_layer


@pytest.mark.parametrize("args", MINIMAL_COMMANDS)
def test_commands_execute(args):
execute(args)
if args.exit_code_no_layer == 0:
execute(args.command)


@pytest.mark.parametrize("args", MINIMAL_COMMANDS)
def test_commands_must_return_document(runner, args):
@pytest.mark.parametrize("cmd", MINIMAL_COMMANDS)
def test_commands_must_return_document(runner, cmd):
@cli.command()
@vp.global_processor
def assertdoc(document):
assert document is not None
assert type(document) is vp.Document

result = runner.invoke(cli, "line 0 0 10 10 " + args + " assertdoc")
assert result.exit_code == 0
result = runner.invoke(cli, "line 0 0 10 10 " + cmd.command + " assertdoc")
assert result.exit_code == cmd.exit_code_one_layer


@pytest.mark.parametrize("args", MINIMAL_COMMANDS)
def test_commands_keeps_page_size(runner, args):
@pytest.mark.parametrize("cmd", MINIMAL_COMMANDS)
def test_commands_keeps_page_size(runner, cmd):
"""No command shall "forget" the current page size, unless its `pagesize` of course."""

args = cmd.command

if args.split()[0] in ["pagesize", "layout"]:
return

Expand All @@ -117,8 +135,10 @@ def getpagesize(doc: vp.Document) -> vp.Document:
page_size = doc.page_size
return doc

result = runner.invoke(cli, "pagesize --landscape 5432x4321 " + args + " getpagesize")
assert result.exit_code == 0
result = runner.invoke(
cli, "random random -l2 pagesize --landscape 5432x4321 " + args + " getpagesize"
)
assert result.exit_code == cmd.exit_code_two_layers
assert page_size == (5432, 4321)


Expand Down Expand Up @@ -449,7 +469,7 @@ def test_snap_no_duplicate(pitch: float):
def test_splitall_filter_duplicates(line, expected):
lc = execute_single_line("splitall", line)

assert np.all(l == el for l, el in zip(lc, expected))
assert np.all(line == expected_line for line, expected_line in zip(lc, expected))


@pytest.mark.parametrize(
Expand Down
32 changes: 32 additions & 0 deletions tests/test_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import math
import random

import click
import numpy as np
import pytest

import vpype as vp
Expand All @@ -28,6 +30,9 @@
("line 0 0 1 1 lmove all new", [2]),
("line 0 0 1 1 line -l new 0 0 1 1 lmove all 2", [2]),
("line 0 0 1 1 line -l new 0 0 1 1 lmove all 3", [3]),
("line 0 0 1 1 lreverse 1", [1]),
("lreverse 1", []), # lreverse doesnt create phantom layers
("line -l2 0 0 1 1 lreverse 1", [2]),
],
)
def test_layer_creation(runner, command, layers):
Expand Down Expand Up @@ -83,6 +88,12 @@ def big_doc():
return doc


def test_layer_not_new():
with pytest.raises(click.exceptions.BadParameter) as exc:
execute("ldelete new")
assert "existing" in exc.value.message


def test_lmove(big_doc):
doc = execute("lmove 1 2", big_doc)

Expand Down Expand Up @@ -176,3 +187,24 @@ def test_lcopy_prob_zero(big_doc):

assert len(doc.layers[1]) == 1000
assert 2 not in doc.layers


def test_lswap():
doc = execute("line -l1 0 0 10 10 line -l2 20 20 30 30 lswap 1 2")

assert np.all(doc.layers[1][0] == np.array([20 + 20j, 30 + 30j]))
assert np.all(doc.layers[2][0] == np.array([0, 10 + 10j]))


def test_lswap_prob_zero():
doc = execute("line -l1 0 0 10 10 line -l2 20 20 30 30 lswap --prob 0. 1 2")

assert np.all(doc.layers[2][0] == np.array([20 + 20j, 30 + 30j]))
assert np.all(doc.layers[1][0] == np.array([0, 10 + 10j]))


def test_lreverse():
doc = execute("line 0 0 10 10 line 20 20 30 30 lreverse 1")

assert np.all(doc.layers[1][0] == np.array([20 + 20j, 30 + 30j]))
assert np.all(doc.layers[1][1] == np.array([0, 10 + 10j]))
28 changes: 24 additions & 4 deletions vpype/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,16 @@ def multiple_to_layer_ids(
return []


def single_to_layer_id(layer: Optional[int], document: Document) -> int:
def single_to_layer_id(
layer: Optional[int], document: Document, must_exist: bool = False
) -> int:
"""Convert single-layer CLI argument to layer ID, accounting for the existence of a current
a current target layer and dealing with default behavior.
Arg:
layer: value from a :class:`LayerType` argument
document: target :class:`Document` instance (for new layer ID)
must_exists: if True, the function
Returns:
Target layer ID
Expand All @@ -71,6 +74,9 @@ def single_to_layer_id(layer: Optional[int], document: Document) -> int:
else:
lid = layer

if must_exist and lid not in document.layers:
raise click.BadParameter(f"layer {layer} does not exist")

return lid


Expand Down Expand Up @@ -113,12 +119,22 @@ def convert(self, value, param, ctx):
if self.accept_multiple:
return LayerType.ALL
else:
self.fail("'all' was not expected", param, ctx)
self.fail(
f"parameter {param.human_readable_name} must be a single layer and does "
"not accept `all`",
param,
ctx,
)
elif value.lower() == "new":
if self.accept_new:
return LayerType.NEW
else:
self.fail("'new' was not expected", param, ctx)
self.fail(
f"parameter {param.human_readable_name} must be an existing layer and "
"does not accept `new`",
param,
ctx,
)

try:
if self.accept_multiple:
Expand All @@ -132,4 +148,8 @@ def convert(self, value, param, ctx):
except TypeError:
self.fail(f"unexpected {value!r} of type {type(value).__name__}", param, ctx)
except ValueError:
self.fail(f"{value!r} is not a valid value", param, ctx)
self.fail(
f"{value!r} is not a valid value for parameter {param.human_readable_name}",
param,
ctx,
)
Loading

0 comments on commit ed26fb6

Please sign in to comment.