From 89bb8084333e0b63d225467297b13718774b42e7 Mon Sep 17 00:00:00 2001 From: jaykv Date: Wed, 25 Jan 2023 14:47:16 -0500 Subject: [PATCH 1/3] Implemented interpolation --- Makefile | 9 +++++++ README.md | 24 ++++++++++++++++++ demo.py | 6 +++++ pybash.py | 68 ++++++++++++++++++++++++++++++++++++-------------- test_pybash.py | 21 ++++++++++++++++ 5 files changed, 109 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index b4283d8..b98ea4b 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,12 @@ install: test: pytest test_pybash.py -vv -rs +dev: + python run.py + +debug: + python -m ideas demo -a pybash -s + clean: rm -rf build/ dist/ *.egg-info .pytest_cache find . -name '*.pyc' -type f -exec rm -rf {} + @@ -30,4 +36,7 @@ lint: flake8 $(SOURCE_FILES) --count --show-source --statistics flake8 $(SOURCE_FILES) --count --exit-zero --statistics +shell: + source $(poetry env info --path)/bin/activate + .PHONY: test clean \ No newline at end of file diff --git a/README.md b/README.md index 001e16b..ecfa2dd 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,30 @@ WHENDY WORLD $ls .github/* ``` +### 10. Interpolation +Denoted by {{variable_or_function_call_here}}. Only static interpolation is supported currently so no quotes, spaces or expressions within the {{}} or in the string being injected. + +```python +# GOOD +command = "status" +def get_option(command): + return "-s" if command == "status" else "-v" +>git {{command}} {{get_option(command)}} + +display_type = "labels" +>k get pods --show-{{display_type}}=true + +# BAD +option = "-s -v" +>git status {{option}} + +options = ['-s', '-v'] +>git status {{" ".join(options)}} + +options = {'version': '-v'} +>git status {{options['version']}} +``` + #### Also works inside methods! ```python # PYBASH DEMO # diff --git a/demo.py b/demo.py index b2901b6..d1f8634 100644 --- a/demo.py +++ b/demo.py @@ -1,5 +1,11 @@ # PYBASH DEMO # def run(): + # command interpolation + git_command = "status" + option = "-v" + + >git {{git_command}} {{option}} + #TODO: #a = >echo "hello" >> test.txt #a = >sort < test.txt | grep "HELLO" diff --git a/pybash.py b/pybash.py index e70944c..fa75d16 100644 --- a/pybash.py +++ b/pybash.py @@ -1,9 +1,30 @@ +import re import shlex import token_utils from ideas import import_hook +@staticmethod +def source_init(): + """Adds subprocess import""" + import_subprocess = "import subprocess" + return import_subprocess + + +@staticmethod +def add_hook(**_kwargs): + """Creates and automatically adds the import hook in sys.meta_path""" + hook = import_hook.create_hook( + hook_name=__name__, transform_source=Transformer.transform_source, source_init=source_init + ) + return hook + + +class InvalidInterpolation(Exception): + pass + + class Processor: command_char = ">" @@ -19,6 +40,26 @@ def parse(self): def transform(self) -> token_utils.Token: raise NotImplementedError + @staticmethod + def interpolate(string: str) -> str: + """Process {{ interpolations }} and substitute. + Interpolations are denotated by a {{ }} with a variable or a function call inside. + + Args: + string (str): String to interpolate + + Returns: + str: Interpolated string + """ + + # validate interpolation + invalid_chars = [' ', '"', "'"] + matches = re.findall(r'{{(.+?)}}', string) + if matches and any(any(bad_char in match for bad_char in invalid_chars) for match in matches): + raise InvalidInterpolation + + return re.sub(r'{{(.+?)}}', r'" + \1 + "', string) + class Shelled(Processor): # $ls .github/* @@ -32,7 +73,6 @@ def transform(self) -> token_utils.Token: class Execed(Processor): # >ls -la - def transform(self) -> token_utils.Token: pipeline_command = Pipeline(self.command).parse_command() if pipeline_command != self.command: @@ -90,8 +130,8 @@ class Transformer: tokenizers = {"$": Shelled, ">": Execed} greedy_tokenizers = {"= >": Variablized, "(>": Wrapped} - @classmethod - def transform_source(cls, source, **_kwargs): + @staticmethod + def transform_source(source, **_kwargs): """Convert >bash commands to subprocess calls""" new_tokens = [] for line in token_utils.get_lines(source): @@ -101,16 +141,20 @@ def transform_source(cls, source, **_kwargs): continue # matches exact token - token_match = [tokenizer for match, tokenizer in cls.tokenizers.items() if token == match] + token_match = [tokenizer for match, tokenizer in Transformer.tokenizers.items() if token == match] if token_match: token = token_match[0](token).transform() + token.string = Processor.interpolate(token.string) new_tokens.append(token) continue # matches anywhere in line - greedy_match = [tokenizer for match, tokenizer in cls.greedy_tokenizers.items() if match in token.line] + greedy_match = [ + tokenizer for match, tokenizer in Transformer.greedy_tokenizers.items() if match in token.line + ] if greedy_match: token = greedy_match[0](token).transform() + token.string = Processor.interpolate(token.string) new_tokens.append(token) continue @@ -385,17 +429,3 @@ def build_subprocess_list_cmd(method: str, args: list, **kwargs) -> str: command += f", {k}={v}" command += ")" return command - - -def source_init(): - """Adds subprocess import""" - import_subprocess = "import subprocess" - return import_subprocess - - -def add_hook(**_kwargs): - """Creates and automatically adds the import hook in sys.meta_path""" - hook = import_hook.create_hook( - hook_name=__name__, transform_source=Transformer.transform_source, source_init=source_init - ) - return hook diff --git a/test_pybash.py b/test_pybash.py index c43a129..0546532 100644 --- a/test_pybash.py +++ b/test_pybash.py @@ -1,3 +1,5 @@ +import pytest + import pybash run_bash = pybash.Transformer.transform_source @@ -95,6 +97,25 @@ def test_shell_commands(): assert run_bash("$ls .github/*") == 'subprocess.run("ls .github/*", shell=True)\n' +def test_interpolate(): + assert run_bash(">git {{command}} {{option}}") == 'subprocess.run(["git","" + command + "","" + option + ""])\n' + assert ( + run_bash(">git {{command}} {{process(option)}}") + == 'subprocess.run(["git","" + command + "","" + process(option) + ""])\n' + ) + assert ( + run_bash(">k get pods --show-{{display_type}}=true") + == 'subprocess.run(["k","get","pods","--show-" + display_type + "=true"])\n' + ) + + +def test_invalid_interpolate(): + with pytest.raises(pybash.InvalidInterpolation): + assert run_bash(">git {{command}} {{ option }}") + assert run_bash(">git {{command}} {{'\"'.join(option)}}") + assert run_bash(">git {{command}} {{a['key']}}") + + def test_no_parse(): assert run_bash('if 5 > 4:') == 'if 5 > 4:' assert run_bash('if (pred1 and pred2) > 0:') == 'if (pred1 and pred2) > 0:' From ddd7b7c0c8ba569d3645df99e0dd7471ebd1eea8 Mon Sep 17 00:00:00 2001 From: jaykv Date: Wed, 25 Jan 2023 14:47:48 -0500 Subject: [PATCH 2/3] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87dd497..a260590 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "PyBash" -version = "0.2.1" +version = "0.2.2" description = ">execute bash commands from python easily" authors = ["Jay"] readme = "README.md" From 5141ecbe921c878a1918aecf6481c75057ca8796 Mon Sep 17 00:00:00 2001 From: jaykv Date: Wed, 25 Jan 2023 14:49:47 -0500 Subject: [PATCH 3/3] Bumped ideas to 0.1.5 --- poetry.lock | 14 +++++++------- pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7bd736f..c7f5ad2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -92,7 +92,7 @@ pyflakes = ">=3.0.0,<3.1.0" [[package]] name = "ideas" -version = "0.1.3" +version = "0.1.5" description = "Easy creation of import hooks to test ideas." category = "main" optional = false @@ -149,7 +149,7 @@ python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.10.3" +version = "0.11.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -242,7 +242,7 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "60145293bdf98ed95e4124adda70073f647c85cb4d6fd579f09320dc885a8b71" +content-hash = "1851ad91ce9e8498716141ae6c414eeb20e6a13733cbf3b4e25523736f884e3a" [metadata.files] attrs = [ @@ -284,8 +284,8 @@ flake8 = [ {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, ] ideas = [ - {file = "ideas-0.1.3-py3-none-any.whl", hash = "sha256:f036f3de0677a5b2c08da708d835baced7890d912380da2974d6d69b27071bf2"}, - {file = "ideas-0.1.3.tar.gz", hash = "sha256:acee64d64e9d05fba05be993413503e93b38ab8f74f9661851bb4a1ab15c54b4"}, + {file = "ideas-0.1.5-py3-none-any.whl", hash = "sha256:894f9db904fd15eb7735d06ecdf77b268f658df778760f8e1ce7bb1e08ac81ad"}, + {file = "ideas-0.1.5.tar.gz", hash = "sha256:20edb786a6f1028e0659e9e51497b395014359baac7ffe74f1d3a89a42414cc0"}, ] iniconfig = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, @@ -308,8 +308,8 @@ packaging = [ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] pathspec = [ - {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, - {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, ] platformdirs = [ {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, diff --git a/pyproject.toml b/pyproject.toml index a260590..90bd685 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ packages = [{include = "pybash.py"}] [tool.poetry.dependencies] python = "^3.9" -ideas = "^0.1.3" +ideas = "^0.1.5" [tool.poetry.group.dev.dependencies] black = "^22.12.0"