From bb0944265b5dedfb55ad7de6d6186aeb6c97b220 Mon Sep 17 00:00:00 2001 From: jerempy Date: Thu, 6 Oct 2022 13:05:39 -0400 Subject: [PATCH 1/4] added callables to [scripts] in Pipfile Can now add a callable like {call = "package.module:func('arg')"} --- pipenv/cmdparse.py | 29 ++++++++++++++++++++++++++++- tests/integration/test_run.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/pipenv/cmdparse.py b/pipenv/cmdparse.py index 673cf80cda..995d7acafb 100644 --- a/pipenv/cmdparse.py +++ b/pipenv/cmdparse.py @@ -1,23 +1,47 @@ import itertools import re import shlex +import tomlkit class ScriptEmptyError(ValueError): pass +class ScriptParseError(ValueError): + pass + + def _quote_if_contains(value, pattern): if next(iter(re.finditer(pattern, value)), None): return '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', value)) return value +def _parse_toml_inline_table(value: tomlkit.items.InlineTable) -> str: + """parses the [scripts] in pipfile and converts: `{call = "package.module:func('arg')"}` into an executable command + """ + keys_list = list(value.keys()) + if len(keys_list) > 1: + raise ScriptParseError("More than 1 key in toml script line") + cmd_key = keys_list[0] + if cmd_key not in Script.script_types: + raise ScriptParseError(f"Not an accepted script callabale, options are: {Script.script_types}") + if cmd_key == "call": + module, _, func = str(value["call"]).partition(":") + if not module or not func: + raise ScriptParseError("Callable must be like: :") + if re.search(r"\(.*?\)", func) is None: + func += "()" + return f"python -c \"import {module} as _m; _m.{func}\"" + + class Script(object): """Parse a script line (in Pipfile's [scripts] section). This always works in POSIX mode, even on Windows. """ + script_types = ["call"] def __init__(self, command, args=None): self._parts = [command] @@ -26,7 +50,10 @@ def __init__(self, command, args=None): @classmethod def parse(cls, value): - if isinstance(value, str): + if isinstance(value, tomlkit.items.InlineTable): + cmd_string = _parse_toml_inline_table(value) + value = shlex.split(cmd_string) + elif isinstance(value, str): value = shlex.split(value) if not value: raise ScriptEmptyError(value) diff --git a/tests/integration/test_run.py b/tests/integration/test_run.py index d09a027b5d..e5c890f798 100644 --- a/tests/integration/test_run.py +++ b/tests/integration/test_run.py @@ -4,6 +4,7 @@ from pipenv.project import Project from pipenv.utils.shell import subprocess_run, temp_environ +from pipenv.utils.shell import mkdir_p @pytest.mark.run @@ -63,6 +64,36 @@ def test_scripts(pipenv_instance_pypi): assert c.stdout.strip() == "WORLD" +@pytest.mark.run +def test_scripts_with_package_functions(pipenv_instance_pypi): + with pipenv_instance_pypi(chdir=True) as p: + p.pipenv('install') + pkg_path = os.path.join(p.path, "pkg") + mkdir_p(pkg_path) + file_path = os.path.join(pkg_path, "mod.py") + with open(file_path, "w+") as f: + f.write(""" +def test_func(): + print("success") + +def arg_func(s, i): + print(f"{s.upper()}. Easy as {i}") +""") + + with open(p.pipfile_path, 'w') as f: + f.write(r""" +[scripts] +pkgfunc = {call = "pkg.mod:test_func"} +argfunc = {call = "pkg.mod:arg_func('abc', 123)"} + """) + + c = p.pipenv('run pkgfunc') + assert c.stdout.strip() == "success" + + c = p.pipenv('run argfunc') + assert c.stdout.strip() == "ABC. Easy as 123" + + @pytest.mark.run @pytest.mark.skip_windows def test_run_with_usr_env_shebang(pipenv_instance_pypi): From 8ff455b4139a19a1dcc0c87a1077fd4bcfa3ff8f Mon Sep 17 00:00:00 2001 From: jerempy Date: Thu, 6 Oct 2022 13:29:09 -0400 Subject: [PATCH 2/4] news seg --- news/5294.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/5294.feature.rst diff --git a/news/5294.feature.rst b/news/5294.feature.rst new file mode 100644 index 0000000000..cf4e7eefb1 --- /dev/null +++ b/news/5294.feature.rst @@ -0,0 +1 @@ +Add ability for callable scripts in Pipfile under [scripts]. Callables can now be added like: ``:`` and can also take arguments. For exaple: ``func = {call = "package.module:func('arg1', 'arg2')"}`` then this can be activated in the shell with ``pipenv run func`` From 4f16fa99e46055683df2a6f1b17ba75b89073dd7 Mon Sep 17 00:00:00 2001 From: jerempy Date: Thu, 6 Oct 2022 13:49:15 -0400 Subject: [PATCH 3/4] lint --- pipenv/cmdparse.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pipenv/cmdparse.py b/pipenv/cmdparse.py index 995d7acafb..cdba6469ec 100644 --- a/pipenv/cmdparse.py +++ b/pipenv/cmdparse.py @@ -1,6 +1,7 @@ import itertools import re import shlex + import tomlkit @@ -19,21 +20,22 @@ def _quote_if_contains(value, pattern): def _parse_toml_inline_table(value: tomlkit.items.InlineTable) -> str: - """parses the [scripts] in pipfile and converts: `{call = "package.module:func('arg')"}` into an executable command - """ + """parses the [scripts] in pipfile and converts: `{call = "package.module:func('arg')"}` into an executable command""" keys_list = list(value.keys()) if len(keys_list) > 1: raise ScriptParseError("More than 1 key in toml script line") cmd_key = keys_list[0] if cmd_key not in Script.script_types: - raise ScriptParseError(f"Not an accepted script callabale, options are: {Script.script_types}") + raise ScriptParseError( + f"Not an accepted script callabale, options are: {Script.script_types}" + ) if cmd_key == "call": module, _, func = str(value["call"]).partition(":") if not module or not func: raise ScriptParseError("Callable must be like: :") if re.search(r"\(.*?\)", func) is None: func += "()" - return f"python -c \"import {module} as _m; _m.{func}\"" + return f'python -c "import {module} as _m; _m.{func}"' class Script(object): @@ -41,6 +43,7 @@ class Script(object): This always works in POSIX mode, even on Windows. """ + script_types = ["call"] def __init__(self, command, args=None): From 5f548d05dd19914d8c0cb17b074259d62f5560d8 Mon Sep 17 00:00:00 2001 From: jerempy Date: Fri, 7 Oct 2022 18:13:11 -0400 Subject: [PATCH 4/4] update vendored lib update to vendored tomlkit everywhere tests were breaking primarily due to the vendored tomlkit classes do not equal the non vendored --- pipenv/cmdparse.py | 2 +- tests/unit/test_vendor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pipenv/cmdparse.py b/pipenv/cmdparse.py index cdba6469ec..0056a09cd1 100644 --- a/pipenv/cmdparse.py +++ b/pipenv/cmdparse.py @@ -2,7 +2,7 @@ import re import shlex -import tomlkit +from pipenv.vendor import tomlkit class ScriptEmptyError(ValueError): diff --git a/tests/unit/test_vendor.py b/tests/unit/test_vendor.py index ee25045260..32e594b3b1 100644 --- a/tests/unit/test_vendor.py +++ b/tests/unit/test_vendor.py @@ -7,7 +7,7 @@ import pytest import pytz -import tomlkit +from pipenv.vendor import tomlkit @pytest.mark.parametrize('dt, content', [