Skip to content

Commit cca7b0f

Browse files
committed
0.0.5: support iterable expression
1 parent 71fd25a commit cca7b0f

File tree

5 files changed

+125
-43
lines changed

5 files changed

+125
-43
lines changed

README.md

+36-12
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ pip install pythoned
1313
```
1414
(it sets up `pythoned` in your PATH)
1515

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

1919
```bash
2020
# get last char of each line
@@ -27,10 +27,10 @@ r
2727
r
2828
```
2929

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

42-
## generate
43-
If the `_` variable is not used in the expression, its value is outputed:
42+
## flatten mode
43+
If the provided expression is an `Iterable`, then its elements will be flattened as separate output lines:
4444
```bash
45-
pythoned '"\n".join(map(str, range(5)))'
45+
# flatten the chars
46+
echo -e 'f00\nbar\nf00bar' | pythoned 'list(_)'
47+
```
48+
output:
49+
```
50+
f
51+
0
52+
0
53+
b
54+
a
55+
r
56+
f
57+
0
58+
0
59+
b
60+
a
61+
r
62+
```
63+
64+
## generator mode
65+
If the `_` variable is not used and the expression is an `Iterable`, then its elements will be separate output lines:
66+
67+
iterables:
68+
```bash
69+
# generate ints
70+
pythoned 'range(5)'
4671
```
4772
output:
4873
```
@@ -55,14 +80,13 @@ output:
5580

5681
## modules
5782

58-
Modules are auto-imported, example with `re`:
83+
Modules are auto-imported, example with `re` and `json`:
5984
```bash
60-
# replace digits by Xs
61-
echo -e 'f00\nbar\nf00bar' | pythoned 're.sub(r"\d", "X", _)'
85+
# replace digits by Xs in the "bar" field
86+
echo -e '{"bar": "f00"}\n{"bar": "foo"}' | pythoned 're.sub(r"\d", "X", json.loads(_)["bar"])'
6287
```
6388
output:
6489
```
6590
fXX
66-
bar
67-
fXXbar
91+
foo
6892
```

pythoned/__init__.py

+38-15
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
import importlib
33
import os
44
import re
5-
from types import ModuleType
6-
from typing import Any, Dict, Iterator, Set
5+
from typing import Any, Dict, Iterable, Iterator, Optional, Set
76

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

119

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

2725

28-
def auto_import_eval(expression: str, globals: Dict[str, Any]) -> Any:
26+
def auto_import_eval(expression: str, globals: Dict[str, Any] = {}) -> Any:
2927
globals = globals.copy()
3028
encountered_name_errors: Set[str] = set()
3129
while True:
@@ -42,19 +40,44 @@ def auto_import_eval(expression: str, globals: Dict[str, Any]) -> Any:
4240
continue
4341

4442

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

49+
50+
def output(value: Any, line: Optional[str] = None, linesep: str = "") -> Iterator[str]:
51+
if isinstance(value, str):
52+
yield value + linesep
53+
elif line is not None and isinstance(value, bool):
54+
if value:
55+
yield line + linesep
56+
elif isinstance(value, Iterable):
57+
yield from output_list(value, linesep)
58+
else:
59+
raise TypeError(
60+
f"the editing expression must be an str (editing) or a bool (filtering) or a iterable (flattening) but got a {type(value)}"
61+
)
62+
63+
64+
def generate(expression: str) -> Iterator[str]:
65+
value = auto_import_eval(expression)
66+
if isinstance(value, Iterable):
67+
yield from output_list(value, linesep=os.linesep)
68+
else:
69+
raise TypeError(
70+
f"the generating expression must be an iterable but got a {type(value)}"
71+
)
72+
73+
74+
def edit(expression: str, lines: Iterator[str]) -> Iterator[str]:
4875
for line in lines:
4976
linesep = ""
5077
if line.endswith(os.linesep):
5178
linesep, line = os.linesep, line[: -len(os.linesep)]
52-
globals = {LINE_IDENTIFIER: line, **modules}
53-
value = auto_import_eval(expression, globals)
54-
if isinstance(value, str):
55-
yield value + linesep
56-
elif isinstance(value, bool):
57-
if value:
58-
yield line + linesep
59-
else:
60-
raise TypeError(_TYPE_ERROR_MSG.format(type(value)))
79+
yield from output(
80+
auto_import_eval(expression, {LINE_IDENTIFIER: line}),
81+
line,
82+
linesep,
83+
)

pythoned/__main__.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import argparse
2+
import os
23
import sys
4+
from typing import Iterator
35

4-
from pythoned import LINE_IDENTIFIER, auto_import_eval, edit, iter_identifiers
6+
from pythoned import (
7+
LINE_IDENTIFIER,
8+
auto_import_eval,
9+
edit,
10+
generate,
11+
iter_identifiers,
12+
output,
13+
)
514

615

716
def main() -> int:
@@ -11,9 +20,10 @@ def main() -> int:
1120
args = arg_parser.parse_args()
1221
expression: str = args.expression
1322
if not LINE_IDENTIFIER in iter_identifiers(expression):
14-
print(auto_import_eval(expression, {}))
23+
for line in generate(expression):
24+
print(line, end="")
1525
else:
16-
for output_line in edit(sys.stdin, expression):
26+
for output_line in edit(expression, sys.stdin):
1727
print(output_line, end="")
1828
return 0
1929

tests/test.py

+37-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Iterator
22
import unittest
33

4-
from pythoned import edit
4+
from pythoned import edit, generate
55

66

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

1111
class TestStream(unittest.TestCase):
1212
def test_edit(self) -> None:
13-
self.assertEqual(
14-
list(edit(lines(), "_[-1]")),
13+
self.assertListEqual(
14+
list(edit("_[-1]", lines())),
1515
["0\n", "r\n", "r"],
1616
msg="str expression must edit the lines",
1717
)
18-
self.assertEqual(
19-
list(edit(lines(), 're.sub(r"\d", "X", _)')),
18+
self.assertListEqual(
19+
list(edit('re.sub(r"\d", "X", _)', lines())),
2020
["fXX\n", "bar\n", "fXXbar"],
2121
msg="re should be supported out-of-the-box",
2222
)
23-
self.assertEqual(
24-
list(edit(lines(), '"0" in _')),
23+
self.assertListEqual(
24+
list(edit('"0" in _', lines())),
2525
["f00\n", "f00bar"],
2626
msg="bool expression must filter the lines",
2727
)
28-
self.assertEqual(
29-
list(edit(lines(), "len(_) == 3")),
28+
self.assertListEqual(
29+
list(edit("list(_)", lines())),
30+
["f\n", "0\n", "0\n", "b\n", "a\n", "r\n", "f\n", "0\n", "0\n", "b\n", "a\n", "r"],
31+
msg="list expression should flatten",
32+
)
33+
self.assertListEqual(
34+
list(edit("len(_) == 3", lines())),
3035
["f00\n", "bar\n"],
31-
msg="_ must exclude linesep",
36+
msg="`_` must not include linesep",
3237
)
33-
self.assertEqual(
34-
list(edit(lines(), "re.sub('[0]', 'O', str(int(math.pow(10, len(_)))))")),
38+
self.assertListEqual(
39+
list(edit("re.sub('[0]', 'O', str(int(math.pow(10, len(_)))))", lines())),
3540
["1OOO\n", "1OOO\n", "1OOOOOO"],
3641
msg="modules should be auto-imported",
3742
)
43+
self.assertListEqual(
44+
list(generate("['foo', 'bar']")),
45+
["foo\n", "bar\n"],
46+
msg="generator when `_` not used",
47+
)
48+
self.assertListEqual(
49+
list(generate("[0, 1]")),
50+
["0\n", "1\n"],
51+
msg="generator when `_` not used, ok with non str elements",
52+
)
53+
with self.assertRaisesRegex(
54+
TypeError,
55+
"the generating expression must be an iterable but got a <class 'bool'>",
56+
):
57+
list(generate("True"))
58+
with self.assertRaisesRegex(
59+
TypeError,
60+
r"the editing expression must be an str \(editing\) or a bool \(filtering\) or a iterable \(flattening\) but got a <class 'int'>",
61+
):
62+
list(edit("0 if _ else 1", lines()))

version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# to show the CHANGELOG: git log -- version.py
2-
__version__ = "0.0.4"
2+
__version__ = "0.0.5"

0 commit comments

Comments
 (0)