diff --git a/README.rst b/README.rst index 8c74954..02dd27e 100644 --- a/README.rst +++ b/README.rst @@ -209,6 +209,12 @@ variable or ``# Requirements:`` section to the script: # Requirements: # requests + # or (PEP 723) + + # /// script + # dependencies = ['requests'] + # /// + import requests req = requests.get('https://pypi.org/project/pip-run') diff --git a/newsfragments/+96.feature.rst b/newsfragments/+96.feature.rst new file mode 100644 index 0000000..0c7f776 --- /dev/null +++ b/newsfragments/+96.feature.rst @@ -0,0 +1 @@ +Add support for script dependencies in a TOML block per PEP 723. \ No newline at end of file diff --git a/pip_run/compat/py310.py b/pip_run/compat/py310.py new file mode 100644 index 0000000..93235ce --- /dev/null +++ b/pip_run/compat/py310.py @@ -0,0 +1,8 @@ +""" +Compatibility for Python 3.10 and earlier. +""" + +try: + import tomllib # type: ignore +except ImportError: # pragma: no cover + import tomli as tomllib # type: ignore diff --git a/pip_run/scripts.py b/pip_run/scripts.py index 36bcf65..ae253d2 100644 --- a/pip_run/scripts.py +++ b/pip_run/scripts.py @@ -11,6 +11,9 @@ from jaraco.context import suppress from jaraco.functools import compose +from .compat.py310 import tomllib + + ValidRequirementString = compose(str, packaging.requirements.Requirement) @@ -73,7 +76,39 @@ def search(cls, params): return cls.try_read(next(files, None)).params() def read(self): - return self.read_comments() or self.read_python() + return self.read_toml() or self.read_comments() or self.read_python() + + def read_toml(self): + r""" + >>> DepsReader('# /// script\n# dependencies = ["foo", "bar"]\n# ///\n').read() + ['foo', 'bar'] + >>> DepsReader('# /// pyproject\n# dependencies = ["foo", "bar"]\n# ///\n').read_toml() + [] + >>> DepsReader('# /// pyproject\n#dependencies = ["foo", "bar"]\n# ///\n').read_toml() + [] + >>> DepsReader('# /// script\n# dependencies = ["foo", "bar"]\n').read_toml() + [] + >>> DepsReader('# /// script\n# ///\n\n# /// script\n# ///').read_toml() + Traceback (most recent call last): + ... + ValueError: Multiple script blocks found + """ + TOML_BLOCK_REGEX = r'(?m)^# /// (?P[a-zA-Z0-9-]+)$\s(?P(^#(| .*)$\s)*)^# ///$' + name = 'script' + matches = list( + filter(lambda m: m.group('type') == name, re.finditer(TOML_BLOCK_REGEX, self.script)) + ) + if len(matches) > 1: + raise ValueError(f'Multiple {name} blocks found') + elif len(matches) == 1: + content = ''.join( + line[2:] if line.startswith('# ') else line[1:] + for line in matches[0].group('content').splitlines(keepends=True) + ) + deps = tomllib.loads(content).get("dependencies", []) + else: + deps = [] + return Dependencies.load(deps) def read_comments(self): r""" diff --git a/setup.cfg b/setup.cfg index c7659ed..9415a46 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ install_requires = jaraco.context jaraco.text platformdirs + tomli; python_version < "3.11" importlib_resources; python_version < "3.9" jaraco.functools >= 3.7 jaraco.env diff --git a/tests/test_scripts.py b/tests/test_scripts.py index eabbd17..5d64910 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -111,6 +111,19 @@ def test_comment_style(self): reqs = scripts.DepsReader(script).read() assert reqs == ['foo==3.1'] + def test_toml_style(self): + script = textwrap.dedent( + """ + #! shebang + + # /// script + # dependencies = ["foo == 3.1"] + # /// + """ + ) + reqs = scripts.DepsReader(script).read() + assert reqs == ['foo==3.1'] + def test_search_long_parameter(self): """ A parameter that is too long to be a filename should not fail.