Skip to content

Commit

Permalink
masonry: support file scripts
Browse files Browse the repository at this point in the history
Resolves: python-poetry#241
  • Loading branch information
peterdeme authored May 6, 2021
1 parent c7e4517 commit 8b1cc5e
Show file tree
Hide file tree
Showing 23 changed files with 249 additions and 25 deletions.
70 changes: 53 additions & 17 deletions poetry/core/json/schemas/poetry-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,17 @@
"scripts": {
"type": "object",
"description": "A hash of scripts to be installed.",
"items": {
"type": "string"
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"oneOf": [
{
"$ref": "#/definitions/script-legacy"
},
{
"$ref": "#/definitions/script-table"
}
]
}
}
},
"plugins": {
Expand Down Expand Up @@ -513,32 +522,59 @@
]
}
},
"scripts": {
"script-table": {
"type": "object",
"patternProperties": {
"^[a-zA-Z-_.0-9]+$": {
"oneOf": [
{
"$ref": "#/definitions/script"
},
{
"$ref": "#/definitions/extra-script"
}
]
"oneOf": [
{
"$ref": "#/definitions/extra-script-legacy"
},
{
"$ref": "#/definitions/extra-scripts"
}
}
]
},
"script": {
"script-legacy": {
"type": "string",
"description": "A simple script pointing to a callable object."
},
"extra-script": {
"extra-scripts": {
"type": "object",
"description": "Either a console entry point or a script file that'll be included in the distribution package.",
"additionalProperties": false,
"properties": {
"reference": {
"type": "string",
"description": "If type is file this is the relative path of the script file, if console it is the module name."
},
"type": {
"description": "Value can be either file or console.",
"type": "string",
"enum": [
"file",
"console"
]
},
"extras": {
"type": "array",
"description": "The required extras for this script. Only applicable if type is console.",
"items": {
"type": "string"
}
}
},
"required": [
"reference",
"type"
]
},
"extra-script-legacy": {
"type": "object",
"description": "A script that should be installed only if extras are activated.",
"additionalProperties": false,
"properties": {
"callable": {
"$ref": "#/definitions/script"
"$ref": "#/definitions/script-legacy",
"description": "The entry point of the script. Deprecated in favour of reference."
},
"extras": {
"type": "array",
Expand Down
63 changes: 59 additions & 4 deletions poetry/core/masonry/builders/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import shutil
import sys
import tempfile
import warnings

from collections import defaultdict
from contextlib import contextmanager
Expand Down Expand Up @@ -285,12 +286,43 @@ def convert_entry_points(self) -> Dict[str, List[str]]:

# Scripts -> Entry points
for name, ep in self._poetry.local_config.get("scripts", {}).items():
extras = ""
if isinstance(ep, dict):
extras: str = ""
module_path: str = ""

# Currently we support 2 legacy and 1 new format:
# (legacy) my_script = 'my_package.main:entry'
# (legacy) my_script = { callable = 'my_package.main:entry' }
# (supported) my_script = { reference = 'my_package.main:entry', type = "console" }

if isinstance(ep, str):
warnings.warn(
"This way of declaring console scripts is deprecated and will be removed in a future version. "
'Use reference = "{}", type = "console" instead.'.format(ep),
DeprecationWarning,
)
extras = ""
module_path = ep
elif isinstance(ep, dict) and (
ep.get("type") == "console"
or "callable" in ep # Supporting both new and legacy format for now
):
if "callable" in ep:
warnings.warn(
"Using the keyword callable is deprecated and will be removed in a future version. "
'Use reference = "{}", type = "console" instead.'.format(
ep["callable"]
),
DeprecationWarning,
)

extras = "[{}]".format(", ".join(ep["extras"]))
ep = ep["callable"]
module_path = ep.get("reference", ep.get("callable"))
else:
continue

result["console_scripts"].append("{} = {}{}".format(name, ep, extras))
result["console_scripts"].append(
"{} = {}{}".format(name, module_path, extras)
)

# Plugins -> entry points
plugins = self._poetry.local_config.get("plugins", {})
Expand All @@ -303,6 +335,29 @@ def convert_entry_points(self) -> Dict[str, List[str]]:

return dict(result)

def convert_script_files(self) -> List[Path]:
script_files: List[Path] = []

for _, ep in self._poetry.local_config.get("scripts", {}).items():
if isinstance(ep, dict) and ep.get("type") == "file":
source = ep["reference"]

if Path(source).is_absolute():
raise RuntimeError(
"{} is an absolute path. Expected relative path.".format(source)
)

abs_path = Path.joinpath(self._path, source)

if not abs_path.exists():
raise RuntimeError("{} file-script is not found.".format(abs_path))
if not abs_path.is_file():
raise RuntimeError("{} file-script is not a file.".format(abs_path))

script_files.append(abs_path)

return script_files

@classmethod
def convert_author(cls, author: str) -> Dict[str, str]:
m = AUTHOR_REGEX.match(author)
Expand Down
9 changes: 9 additions & 0 deletions poetry/core/masonry/builders/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ def build_setup(self) -> bytes:
before.append("entry_points = \\\n{}\n".format(pformat(entry_points)))
extra.append("'entry_points': entry_points,")

script_files = self.convert_script_files()
if script_files:
rel_paths = [str(p.relative_to(self._path)) for p in script_files]
before.append('scripts = \\\n["{}"]\n'.format('", "'.join(rel_paths)))
extra.append("'scripts': scripts,")

if self._package.python_versions != "*":
python_requires = self._meta.requires_python

Expand Down Expand Up @@ -314,6 +320,9 @@ def find_files_to_add(self, exclude_build: bool = False) -> Set[BuildIncludeFile
license_file for license_file in self._path.glob("LICENSE*")
}

# add script files
additional_files.update(self.convert_script_files())

# Include project files
additional_files.add("pyproject.toml")

Expand Down
15 changes: 15 additions & 0 deletions poetry/core/masonry/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def build(self) -> None:
self._copy_module(zip_file)
self._build(zip_file)

self._copy_file_scripts(zip_file)
self._write_metadata(zip_file)
self._write_record(zip_file)

Expand Down Expand Up @@ -164,6 +165,16 @@ def _build(self, wheel: zipfile.ZipFile) -> None:

self._add_file(wheel, pkg, rel_path)

def _copy_file_scripts(self, wheel: zipfile.ZipFile) -> None:
file_scripts = self.convert_script_files()

for abs_path in file_scripts:
self._add_file(
wheel,
abs_path,
Path.joinpath(Path(self.wheel_data_folder), "scripts", abs_path.name),
)

def _run_build_command(self, setup: Path) -> None:
subprocess.check_call(
[
Expand Down Expand Up @@ -238,6 +249,10 @@ def _write_record(self, wheel: zipfile.ZipFile) -> None:
def dist_info(self) -> str:
return self.dist_info_name(self._package.name, self._meta.version)

@property
def wheel_data_folder(self) -> str:
return "{}-{}.data".format(self._package.name, self._meta.version)

@property
def wheel_filename(self) -> str:
return "{}-{}-{}.whl".format(
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/complete.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pytest-cov = "^2.4"

[tool.poetry.scripts]
my-script = 'my_package:main'
sample_pyscript = { reference = "script-files/sample_script.py", type= "file" }
sample_shscript = { reference = "script-files/sample_script.sh", type= "file" }


[[tool.poetry.source]]
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/script-files/sample_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python

hello = "Hello World!"
3 changes: 3 additions & 0 deletions tests/fixtures/script-files/sample_script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo "Hello World!"
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ time = ["pendulum"]
[tool.poetry.scripts]
my-script = "my_package:main"
my-2nd-script = "my_package:main2"
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}
3 changes: 3 additions & 0 deletions tests/masonry/builders/fixtures/complete/bin/script1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo "Hello World!"
5 changes: 4 additions & 1 deletion tests/masonry/builders/fixtures/complete/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ time = ["pendulum"]
[tool.poetry.scripts]
my-script = "my_package:main"
my-2nd-script = "my_package:main2"
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
extra-script-legacy = {callable = "my_package.extra_legacy:main", extras = ["time"]}
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}
sh-script = {reference = "bin/script1.sh", type = "file"}


[tool.poetry.urls]
"Issue Tracker" = "https://github.com/python-poetry/poetry/issues"
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ time = ["pendulum"]
[tool.poetry.scripts]
my-script = "my_package:main"
my-2nd-script = "my_package:main2"
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ time = ["pendulum"]
[tool.poetry.scripts]
my-script = "my_package:main"
my-2nd-script = "my_package:main2"
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}

[tool.poetry.urls]
"Issue Tracker" = "https://github.com/python-poetry/poetry/issues"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Missing Script Files
========
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.poetry]
name = "missing-script-files"
version = "0.1"
description = "Some description."
authors = [
"Sébastien Eustace <[email protected]>"
]
readme = "README.rst"

[tool.poetry.scripts]
missing_file = {reference = "not_existing_folder/not_existing_file.sh", type = "file"}


[tool.poetry.dependencies]
python = "3.6"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Script File Invalid Definition
========

This is a use case where the user provides a pyproject.toml where the file script definition is wrong.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
echo "Hello World"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.poetry]
name = "script_file_invalid_definition"
version = "0.1"
description = "Some description."
authors = [
"Sébastien Eustace <[email protected]>"
]
readme = "README.rst"

[tool.poetry.scripts]
invalid_definition = {reference = "bin/script.sh", type = "ffiillee"}


[tool.poetry.dependencies]
python = "3.6"
25 changes: 25 additions & 0 deletions tests/masonry/builders/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,28 @@ def test_metadata_with_url_dependencies():
"demo @ https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"
== requires_dist
)


def test_missing_script_files_throws_error():
builder = Builder(
Factory().create_poetry(
Path(__file__).parent / "fixtures" / "missing_script_files"
)
)

with pytest.raises(RuntimeError) as err:
builder.convert_script_files()

assert "file-script is not found." in err.value.args[0]


def test_invalid_script_files_definition():
with pytest.raises(RuntimeError) as err:
Builder(
Factory().create_poetry(
Path(__file__).parent / "fixtures" / "script_file_invalid_definition"
)
)

assert "configuration is invalid" in err.value.args[0]
assert "[scripts.invalid_definition]" in err.value.args[0]
Loading

0 comments on commit 8b1cc5e

Please sign in to comment.