From f6a6fade09dba36f417d597b9928cf1d58cf0cf2 Mon Sep 17 00:00:00 2001 From: ebonnal Date: Tue, 18 Mar 2025 23:22:34 +0000 Subject: [PATCH] 0.0.5: support iterable expression --- README.md | 48 +++++++++++++++++++++++++++++---------- pythoned/__init__.py | 53 +++++++++++++++++++++++++++++++------------- pythoned/__main__.py | 16 ++++++++++--- tests/test.py | 49 ++++++++++++++++++++++++++++++---------- version.py | 2 +- 5 files changed, 125 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index fa7311b..34bf1d3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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: ``` @@ -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 ``` diff --git a/pythoned/__init__.py b/pythoned/__init__.py index 982b9b6..fb24498 100644 --- a/pythoned/__init__.py +++ b/pythoned/__init__.py @@ -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]: @@ -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: @@ -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, + ) diff --git a/pythoned/__main__.py b/pythoned/__main__.py index 2eb548e..dd76cc4 100644 --- a/pythoned/__main__.py +++ b/pythoned/__main__.py @@ -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: @@ -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 diff --git a/tests/test.py b/tests/test.py index b401e0e..92bfb2a 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,7 +1,7 @@ from typing import Iterator import unittest -from pythoned import edit +from pythoned import edit, generate def lines() -> Iterator[str]: @@ -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 ", + ): + 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 ", + ): + list(edit("0 if _ else 1", lines())) diff --git a/version.py b/version.py index 739378a..9c60de3 100644 --- a/version.py +++ b/version.py @@ -1,2 +1,2 @@ # to show the CHANGELOG: git log -- version.py -__version__ = "0.0.4" +__version__ = "0.0.5"