Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added script file feature #40

Merged
merged 3 commits into from
May 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
========
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 <sebastien@eustace.io>"
]
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 <sebastien@eustace.io>"
]
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