Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pip install pythoned
```
(it sets up `pythoned` in your PATH)

## edit
You provide a Python `str` expression, manipulating the line stored in the `_: str` variable:
## edit mode
You provide a Python `str` expression, manipulating the line stored in the `_` variable (an `str`):

```bash
# get last char of each line
Expand All @@ -27,10 +27,10 @@ r
r
```

## filter
## filter mode
If the provided expression is a `bool` instead of an `str`, then the lines will be filtered according to it:
```bash
# keep only lines whose length equals 3
# keep only lines containing 2 consecutive zeros
echo -e 'f00\nbar\nf00bar' | pythoned '"00" in _'
```
output:
Expand All @@ -39,10 +39,35 @@ f00
f00bar
```

## generate
If the `_` variable is not used in the expression, its value is outputed:
## flatten mode
If the provided expression is an `Iterable`, then its elements will be flattened as separate output lines:
```bash
pythoned '"\n".join(map(str, range(5)))'
# flatten the chars
echo -e 'f00\nbar\nf00bar' | pythoned 'list(_)'
```
output:
```
f
0
0
b
a
r
f
0
0
b
a
r
```

## generator mode
If the `_` variable is not used and the expression is an `Iterable`, then its elements will be separate output lines:

iterables:
```bash
# generate ints
pythoned 'range(5)'
```
output:
```
Expand All @@ -55,14 +80,13 @@ output:

## modules

Modules are auto-imported, example with `re`:
Modules are auto-imported, example with `re` and `json`:
```bash
# replace digits by Xs
echo -e 'f00\nbar\nf00bar' | pythoned 're.sub(r"\d", "X", _)'
# replace digits by Xs in the "bar" field
echo -e '{"bar": "f00"}\n{"bar": "foo"}' | pythoned 're.sub(r"\d", "X", json.loads(_)["bar"])'
```
output:
```
fXX
bar
fXXbar
foo
```
53 changes: 38 additions & 15 deletions pythoned/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
import importlib
import os
import re
from types import ModuleType
from typing import Any, Dict, Iterator, Set
from typing import Any, Dict, Iterable, Iterator, Optional, Set

LINE_IDENTIFIER = "_"
_TYPE_ERROR_MSG = "The provided expression must be an str (editing) or a bool (filtering), but got {}."


def iter_identifiers(expr: str) -> Iterator[str]:
Expand All @@ -25,7 +23,7 @@ def iter_asts(node: ast.AST) -> Iterator[ast.AST]:
)


def auto_import_eval(expression: str, globals: Dict[str, Any]) -> Any:
def auto_import_eval(expression: str, globals: Dict[str, Any] = {}) -> Any:
globals = globals.copy()
encountered_name_errors: Set[str] = set()
while True:
Expand All @@ -42,19 +40,44 @@ def auto_import_eval(expression: str, globals: Dict[str, Any]) -> Any:
continue


def edit(lines: Iterator[str], expression) -> Iterator[str]:
modules: Dict[str, ModuleType] = {}
def output_list(value: Iterable[str], linesep: str) -> Iterator[str]:
value = list(map(str, value))
for item in value[:-1]:
yield item + os.linesep
yield value[-1] + linesep


def output(value: Any, line: Optional[str] = None, linesep: str = "") -> Iterator[str]:
if isinstance(value, str):
yield value + linesep
elif line is not None and isinstance(value, bool):
if value:
yield line + linesep
elif isinstance(value, Iterable):
yield from output_list(value, linesep)
else:
raise TypeError(
f"the editing expression must be an str (editing) or a bool (filtering) or a iterable (flattening) but got a {type(value)}"
)


def generate(expression: str) -> Iterator[str]:
value = auto_import_eval(expression)
if isinstance(value, Iterable):
yield from output_list(value, linesep=os.linesep)
else:
raise TypeError(
f"the generating expression must be an iterable but got a {type(value)}"
)


def edit(expression: str, lines: Iterator[str]) -> Iterator[str]:
for line in lines:
linesep = ""
if line.endswith(os.linesep):
linesep, line = os.linesep, line[: -len(os.linesep)]
globals = {LINE_IDENTIFIER: line, **modules}
value = auto_import_eval(expression, globals)
if isinstance(value, str):
yield value + linesep
elif isinstance(value, bool):
if value:
yield line + linesep
else:
raise TypeError(_TYPE_ERROR_MSG.format(type(value)))
yield from output(
auto_import_eval(expression, {LINE_IDENTIFIER: line}),
line,
linesep,
)
16 changes: 13 additions & 3 deletions pythoned/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import argparse
import os
import sys
from typing import Iterator

from pythoned import LINE_IDENTIFIER, auto_import_eval, edit, iter_identifiers
from pythoned import (
LINE_IDENTIFIER,
auto_import_eval,
edit,
generate,
iter_identifiers,
output,
)


def main() -> int:
Expand All @@ -11,9 +20,10 @@ def main() -> int:
args = arg_parser.parse_args()
expression: str = args.expression
if not LINE_IDENTIFIER in iter_identifiers(expression):
print(auto_import_eval(expression, {}))
for line in generate(expression):
print(line, end="")
else:
for output_line in edit(sys.stdin, expression):
for output_line in edit(expression, sys.stdin):
print(output_line, end="")
return 0

Expand Down
49 changes: 37 additions & 12 deletions tests/test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Iterator
import unittest

from pythoned import edit
from pythoned import edit, generate


def lines() -> Iterator[str]:
Expand All @@ -10,28 +10,53 @@ def lines() -> Iterator[str]:

class TestStream(unittest.TestCase):
def test_edit(self) -> None:
self.assertEqual(
list(edit(lines(), "_[-1]")),
self.assertListEqual(
list(edit("_[-1]", lines())),
["0\n", "r\n", "r"],
msg="str expression must edit the lines",
)
self.assertEqual(
list(edit(lines(), 're.sub(r"\d", "X", _)')),
self.assertListEqual(
list(edit('re.sub(r"\d", "X", _)', lines())),
["fXX\n", "bar\n", "fXXbar"],
msg="re should be supported out-of-the-box",
)
self.assertEqual(
list(edit(lines(), '"0" in _')),
self.assertListEqual(
list(edit('"0" in _', lines())),
["f00\n", "f00bar"],
msg="bool expression must filter the lines",
)
self.assertEqual(
list(edit(lines(), "len(_) == 3")),
self.assertListEqual(
list(edit("list(_)", lines())),
["f\n", "0\n", "0\n", "b\n", "a\n", "r\n", "f\n", "0\n", "0\n", "b\n", "a\n", "r"],
msg="list expression should flatten",
)
self.assertListEqual(
list(edit("len(_) == 3", lines())),
["f00\n", "bar\n"],
msg="_ must exclude linesep",
msg="`_` must not include linesep",
)
self.assertEqual(
list(edit(lines(), "re.sub('[0]', 'O', str(int(math.pow(10, len(_)))))")),
self.assertListEqual(
list(edit("re.sub('[0]', 'O', str(int(math.pow(10, len(_)))))", lines())),
["1OOO\n", "1OOO\n", "1OOOOOO"],
msg="modules should be auto-imported",
)
self.assertListEqual(
list(generate("['foo', 'bar']")),
["foo\n", "bar\n"],
msg="generator when `_` not used",
)
self.assertListEqual(
list(generate("[0, 1]")),
["0\n", "1\n"],
msg="generator when `_` not used, ok with non str elements",
)
with self.assertRaisesRegex(
TypeError,
"the generating expression must be an iterable but got a <class 'bool'>",
):
list(generate("True"))
with self.assertRaisesRegex(
TypeError,
r"the editing expression must be an str \(editing\) or a bool \(filtering\) or a iterable \(flattening\) but got a <class 'int'>",
):
list(edit("0 if _ else 1", lines()))
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# to show the CHANGELOG: git log -- version.py
__version__ = "0.0.4"
__version__ = "0.0.5"