Skip to content

Commit

Permalink
- updated README.md
Browse files Browse the repository at this point in the history
- added substitution to LayerType
- added `--no-fail` to read
- added glob and convert_{length, angle, page_size} to expr symbols
- disabled numpy from expr
- grid now sets page size
- fixed logging to print fully substituted arguments
- added args logging to @block_processor
- added example for interactive grid
- doc: renamed metadata section to properties
  • Loading branch information
abey79 committed Feb 15, 2022
1 parent 2e4a2cf commit ffa53c2
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 165 deletions.
15 changes: 8 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ New features and improvements:
```bash
$ vpype read input.svg text --layer 1 "Name: {vp_name} Pen width: {vp_pen_width:.2f}" write output.svg
```
See the [documentation](https://vpype.readthedocs.io/en/latest/fundamentals.html#cli-property-substitution) for more information and examples.
See the [documentation](https://vpype.readthedocs.io/en/latest/fundamentals.html#property-substitution) for more information and examples.

* Added expression substitution to CLI user input (#397)

Expand Down Expand Up @@ -54,12 +54,11 @@ New features and improvements:
* When `--prob` is not used, the `lswap` command now swaps the layer properties as well.
* These behaviors can be disabled with the `--no-prop` option.

* Improved the handling of block processors (#395, #397)

Block processors are commands which, when combined with `begin` and `end`, operate on the sequence they encompass. For example, the sequence `begin grid 2 2 random end` creates a 2x2 grid of random line patches. The infrastructure underlying block processors has been overhauled to increase their usefulness and extensibility.

* The `grid` block processor now sets variables for expressions in nested commands.
* The `repeat` block processor now sets variables for expressions in nested commands.
* Improved block processors (#395, #397)

* Simplified and improved the infrastructure underlying block processors for better extensibility.
* The `grid` block processor now adjusts the page size according to its layout.
* The `grid` and `repeat` block processors now sets variables for expressions in nested commands.
* Added `forfile` block processor to iterate over a list of file.
* Added `forlayer` block processor to iterate over the existing layers.
* The `begin` marker is now optional and implied whenever a block processor command is encountered. The following pipelines are thus equivalent:
Expand All @@ -70,6 +69,8 @@ New features and improvements:
*Note*: the `end` marker must always be used to mark the end of a block.
* Commands inside the block now have access to the current layer structure and its metadata. This makes their use more predictable. For example, `begin grid 2 2 random --layer new end` now correctly generates patches of random lines on different layers.
* The `grid` block processor now first iterate along lines instead of columns.

* The `read` command now will ignore a missing file if `--no-fail` parameter is used (#397)

* Changed the initial default target layer to 1 (#395)

Expand Down
184 changes: 120 additions & 64 deletions README.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions docs/fundamentals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ Likewise, angles are interpreted as degrees by default but alternative units may

.. _fundamentals_metadata:

Metadata
========
Properties
==========

Metadata is data which provides information about other data. In the case of *vpype*, metadata takes the form of *properties* that are either attached to a given layer, or global. Properties are identified by a name and their value can be of arbitrary type (e.g. integer, floating point, color, etc.). There can be any number of global and/or layer properties and it is up to commands (and plug-ins) how they act based (or upon) these properties.
In addition to geometries, the *vpype* pipeline carries metadata, i.e. data that provides information about geometries. This metadata takes the form of *properties* that are either attached to a given layer, or global. Properties are identified by a name and their value can be of arbitrary type (e.g. integer, floating point, color, etc.). There can be any number of global and/or layer properties and it is up to commands (and plug-ins) how they act based (or upon) these properties.


System properties
Expand Down Expand Up @@ -201,8 +201,8 @@ High-level commands such as :ref:`cmd_penwidth` are not the only means of intera

.. _fundamentals_property_substitution:

CLI property substitution
-------------------------
Property substitution
---------------------

Most arguments and options passes to commands via the *vpype* CLI will apply property substitution on the provided input. For example, this command will draw the name of the layer::

Expand Down
21 changes: 21 additions & 0 deletions examples/grid.vpy
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Ask user for some information, using sensible defaults.
eval "files=glob(input('Files [*.svg]? ') or '*.svg')" # glob() creates a list of file based on a pattern
eval "cols=int(input('Number of columns [3]? ') or 3)"
eval "rows=ceil(len(files)/cols)" # the number of rows depends on the number of files
eval "col_width=convert_length(input('Column width [10cm]? ') or '10cm')" # convert_length() converts string like '3cm' to pixels
eval "row_height=convert_length(input('Row height [10cm]? ') or '10cm')"
eval "margin=convert_length(input('Margin [0.5cm]? ') or '0.5cm')"
eval "output_path=input('Output path [output.svg]? ') or 'output.svg'"

# Create a grid with provided parameters.
grid -o %col_width% %row_height% %cols% %rows%

# Read the `_i`-th file. The last row may be incomplete so we use an empty path and `--no-fail`.
read --no-fail "%files[_i] if _i < len(files) else ''%"

# Layout the file in the cell.
layout -m %margin% %col_width%x%row_height%
end

# wWrite the output file.
write "%output_path%"
16 changes: 9 additions & 7 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ class Command:
exit_code_one_layer: int = 0
exit_code_two_layers: int = 0
preserves_metadata: bool = True
keeps_page_size: bool = True


MINIMAL_COMMANDS = [
Command("begin grid 2 2 line 0 0 10 10 end"),
Command("begin grid 2 2 line 0 0 10 10 end", keeps_page_size=False),
Command("begin grid 0 0 line 0 0 10 10 end"), # doesn't update page size
Command("begin repeat 2 line 0 0 10 10 end"),
Command("grid 2 2 line 0 0 10 10 end"), # implicit `begin`
Command("grid 2 2 repeat 2 random -n 1 end end"), # nested block
Command("grid 2 2 line 0 0 10 10 end", keeps_page_size=False), # implicit `begin`
Command("grid 2 2 repeat 2 random -n 1 end end", keeps_page_size=False), # nested block
Command("frame"),
Command("random"),
Command("line 0 0 1 1"),
Expand Down Expand Up @@ -70,11 +72,11 @@ class Command:
Command("trim 1mm 1mm"),
Command("splitall"),
Command("filter --min-length 1mm"),
Command("pagesize 10inx15in"),
Command("pagesize 10inx15in", keeps_page_size=False),
Command("stat"),
Command("snap 1"),
Command("reverse"),
Command("layout a4"),
Command("layout a4", keeps_page_size=False),
Command("squiggles"),
Command("text 'hello wold'"),
Command("penwidth 0.15mm", preserves_metadata=False),
Expand All @@ -88,7 +90,7 @@ class Command:
Command("proplist -l 1"),
Command("propdel -g prop:global", preserves_metadata=False),
Command("propdel -l 1 prop:layer", preserves_metadata=False),
Command("propclear -g", preserves_metadata=False),
Command("propclear -g", preserves_metadata=False, keeps_page_size=False),
Command("propclear -l 1", preserves_metadata=False),
Command(
f"forfile '{EXAMPLE_SVG_DIR / '*.svg'}' text -p 0 %_i*cm% '%_i%/%_n%: %_name%' end"
Expand Down Expand Up @@ -153,7 +155,7 @@ def test_commands_keeps_page_size(runner, cmd):

args = cmd.command

if args.split()[0] in ["pagesize", "layout"] or args.startswith("propclear -g"):
if not cmd.keeps_page_size:
pytest.skip(f"command {args.split()[0]} fail this test by design")

page_size = None
Expand Down
7 changes: 7 additions & 0 deletions tests/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
from typing import Set

import click
import numpy as np
import pytest

Expand Down Expand Up @@ -423,3 +424,9 @@ def test_write_svg_svg_props_unknown_namespace(capsys):
"line 0 0 10 10 layout a5 propset -g svg_unknown_version '1.1.0' write -r -f svg -"
)
assert 'unknown:version="1.1.0"' not in capsys.readouterr().out


def test_read_no_fail():
with pytest.raises(click.BadParameter):
vpype_cli.execute("read doesnotexist.svg")
vpype_cli.execute("read --no-fail doesnotexist.svg")
2 changes: 2 additions & 0 deletions vpype_cli/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def grid(
execute_processors(processors, state)
doc.translate(offset[0] * i, offset[1] * j)
state.document.extend(doc)
if nx > 0 and ny > 0:
state.document.page_size = (nx * offset[0], ny * offset[1])

return state

Expand Down
29 changes: 16 additions & 13 deletions vpype_cli/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,17 @@ def new_func(*args, **kwargs):

# noinspection PyShadowingNames
def layer_processor(state: State) -> State:
for lid in multiple_to_layer_ids(layers, state.document):
logging.info(
f"executing layer processor `{f.__name__}` on layer {lid} "
f"(kwargs: {kwargs})"
)
layers_eval = state.preprocess_argument(layers)

for lid in multiple_to_layer_ids(layers_eval, state.document):
start = datetime.datetime.now()
with state.current():
state.current_layer_id = lid
new_args, new_kwargs = state.preprocess_arguments(args, kwargs)
logging.info(
f"executing layer processor `{f.__name__}` on layer {lid} "
f"(kwargs: {new_kwargs})"
)
state.document[lid] = f(state.document[lid], *new_args, **new_kwargs)
state.current_layer_id = None
stop = datetime.datetime.now()
Expand Down Expand Up @@ -148,11 +149,12 @@ def my_global_processor(
def new_func(*args, **kwargs):
# noinspection PyShadowingNames
def global_processor(state: State) -> State:
logging.info(f"executing global processor `{f.__name__}` (kwargs: {kwargs})")

start = datetime.datetime.now()
with state.current():
new_args, new_kwargs = state.preprocess_arguments(args, kwargs)
logging.info(
f"executing global processor `{f.__name__}` (kwargs: {new_kwargs})"
)
state.document = f(state.document, *new_args, **new_kwargs)
stop = datetime.datetime.now()

Expand Down Expand Up @@ -189,16 +191,16 @@ def new_func(*args, **kwargs):
# noinspection PyShadowingNames
def generator(state: State) -> State:
with state.current():
target_layer = single_to_layer_id(layer, state.document)

logging.info(
f"executing generator `{f.__name__}` to layer {target_layer} "
f"(kwargs: {kwargs})"
)
layer_eval = state.preprocess_argument(layer)
target_layer = single_to_layer_id(layer_eval, state.document)

start = datetime.datetime.now()
state.current_layer_id = target_layer
new_args, new_kwargs = state.preprocess_arguments(args, kwargs)
logging.info(
f"executing generator `{f.__name__}` to layer {target_layer} "
f"(kwargs: {new_kwargs})"
)
state.document.add(f(*new_args, **new_kwargs), target_layer)
state.current_layer_id = None
stop = datetime.datetime.now()
Expand Down Expand Up @@ -248,6 +250,7 @@ def block_processor(state: State, processors: Iterable["ProcessorType"]) -> Stat

start = datetime.datetime.now()
new_args, new_kwargs = state.preprocess_arguments(args, kwargs)
logging.info(f"executing block processor `{f.__name__}` (kwargs: {new_kwargs})")
f(state, processors, *new_args, **new_kwargs)
stop = datetime.datetime.now()

Expand Down
11 changes: 10 additions & 1 deletion vpype_cli/read.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import pathlib
import sys
from typing import List, Optional, Tuple

Expand All @@ -14,7 +15,7 @@


@cli.command(group="Input")
@click.argument("file", type=PathType(exists=True, dir_okay=False, allow_dash=True))
@click.argument("file", type=PathType(dir_okay=False, allow_dash=True))
@click.option("-m", "--single-layer", is_flag=True, help="Single layer mode.")
@click.option(
"-l",
Expand All @@ -36,6 +37,7 @@
default="0.1mm",
help="Maximum length of segments approximating curved elements (default: 0.1mm).",
)
@click.option("--no-fail", is_flag=True, help="Do not fail is the target file doesn't exist.")
@click.option(
"-s",
"--simplify",
Expand Down Expand Up @@ -82,6 +84,7 @@ def read(
layer: Optional[int],
attr: List[str],
quantization: float,
no_fail: bool,
simplify: bool,
parallel: bool,
no_crop: bool,
Expand Down Expand Up @@ -188,6 +191,12 @@ def read(

if file == "-":
file = sys.stdin
elif not pathlib.Path(file).is_file():
if no_fail:
logging.debug("read: file doesn't exist, ignoring due to `--no-fail`")
return document
else:
raise click.BadParameter(f"file {file!r} does not exist")

if layer is not None and not single_layer:
single_layer = True
Expand Down
24 changes: 18 additions & 6 deletions vpype_cli/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ class _DeferredEvaluator(ABC):
instances, perform the conversion, and forward the converted value to the command function.
"""

def __init__(self, text: str):
def __init__(self, text: str, param_name: str, *args, **kwargs):
self._text = text
self._param_name = param_name

@abstractmethod
def evaluate(self, state: "State") -> Any:
Expand Down Expand Up @@ -71,21 +72,32 @@ def __init__(self, document: Optional[vp.Document] = None):

self._interpreter = SubstitutionHelper(self)

def _preprocess_arg(self, arg: Any) -> Any:
def preprocess_argument(self, arg: Any) -> Any:
"""Evaluate an argument.
If ``arg`` is a :class:`_DeferredEvaluator` instance, evaluate it a return its value
instead.
Args:
arg: argument to evaluate
Returns:
returns the fully evaluated ``arg``
"""
if isinstance(arg, tuple):
return tuple(self._preprocess_arg(item) for item in arg)
return tuple(self.preprocess_argument(item) for item in arg)
else:
return arg.evaluate(self) if isinstance(arg, _DeferredEvaluator) else arg

def preprocess_arguments(
self, args: Tuple[Any, ...], kwargs: Dict[str, Any]
) -> Tuple[Tuple[Any, ...], Dict[str, Any]]:
"""Replace any instance of :class:`_DeferredEvaluator` and replace them with the
"""Evaluate any instance of :class:`_DeferredEvaluator` and replace them with the
converted value.
"""
return (
tuple(self._preprocess_arg(arg) for arg in args),
{k: self._preprocess_arg(v) for k, v in kwargs.items()},
tuple(self.preprocess_argument(arg) for arg in args),
{k: self.preprocess_argument(v) for k, v in kwargs.items()},
)

def substitute(self, text: str) -> str:
Expand Down
17 changes: 15 additions & 2 deletions vpype_cli/substitution.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import glob
import os
import pathlib
import sys
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Optional
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional

import asteval

Expand Down Expand Up @@ -205,6 +207,12 @@ def _substitute_expressions(
return "".join(_split_text(text, prop_interpreter, expr_interpreter))


def _glob(files: str) -> List[pathlib.Path]:
return [
pathlib.Path(file) for file in glob.glob(os.path.expandvars(os.path.expanduser(files)))
]


_OS_PATH_SYMBOLS = (
"abspath",
"basename",
Expand All @@ -231,14 +239,19 @@ def __init__(self, state: "State"):
**vp.UNITS,
**{f: getattr(os.path, f) for f in _OS_PATH_SYMBOLS},
"input": input,
"glob": _glob,
"convert_length": vp.convert_length,
"convert_angle": vp.convert_angle,
"convert_page_size": vp.convert_page_size,
"stdin": sys.stdin,
"Color": vp.Color,
"prop": self._property_proxy,
"lprop": _PropertyProxy(state, False, True),
"gprop": _PropertyProxy(state, True, False),
}
# disabling numpy as its math functions such as `ceil` do not convert to int.
self._interpreter = asteval.Interpreter(
usersyms=symtable, readonly_symbols=symtable.keys()
usersyms=symtable, readonly_symbols=symtable.keys(), use_numpy=False
)

@property
Expand Down
Loading

0 comments on commit ffa53c2

Please sign in to comment.