Skip to content

Commit 8b1cc5e

Browse files
authored
masonry: support file scripts
Resolves: #241
1 parent c7e4517 commit 8b1cc5e

File tree

23 files changed

+249
-25
lines changed

23 files changed

+249
-25
lines changed

poetry/core/json/schemas/poetry-schema.json

+53-17
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,17 @@
163163
"scripts": {
164164
"type": "object",
165165
"description": "A hash of scripts to be installed.",
166-
"items": {
167-
"type": "string"
166+
"patternProperties": {
167+
"^[a-zA-Z-_.0-9]+$": {
168+
"oneOf": [
169+
{
170+
"$ref": "#/definitions/script-legacy"
171+
},
172+
{
173+
"$ref": "#/definitions/script-table"
174+
}
175+
]
176+
}
168177
}
169178
},
170179
"plugins": {
@@ -513,32 +522,59 @@
513522
]
514523
}
515524
},
516-
"scripts": {
525+
"script-table": {
517526
"type": "object",
518-
"patternProperties": {
519-
"^[a-zA-Z-_.0-9]+$": {
520-
"oneOf": [
521-
{
522-
"$ref": "#/definitions/script"
523-
},
524-
{
525-
"$ref": "#/definitions/extra-script"
526-
}
527-
]
527+
"oneOf": [
528+
{
529+
"$ref": "#/definitions/extra-script-legacy"
530+
},
531+
{
532+
"$ref": "#/definitions/extra-scripts"
528533
}
529-
}
534+
]
530535
},
531-
"script": {
536+
"script-legacy": {
532537
"type": "string",
533538
"description": "A simple script pointing to a callable object."
534539
},
535-
"extra-script": {
540+
"extra-scripts": {
541+
"type": "object",
542+
"description": "Either a console entry point or a script file that'll be included in the distribution package.",
543+
"additionalProperties": false,
544+
"properties": {
545+
"reference": {
546+
"type": "string",
547+
"description": "If type is file this is the relative path of the script file, if console it is the module name."
548+
},
549+
"type": {
550+
"description": "Value can be either file or console.",
551+
"type": "string",
552+
"enum": [
553+
"file",
554+
"console"
555+
]
556+
},
557+
"extras": {
558+
"type": "array",
559+
"description": "The required extras for this script. Only applicable if type is console.",
560+
"items": {
561+
"type": "string"
562+
}
563+
}
564+
},
565+
"required": [
566+
"reference",
567+
"type"
568+
]
569+
},
570+
"extra-script-legacy": {
536571
"type": "object",
537572
"description": "A script that should be installed only if extras are activated.",
538573
"additionalProperties": false,
539574
"properties": {
540575
"callable": {
541-
"$ref": "#/definitions/script"
576+
"$ref": "#/definitions/script-legacy",
577+
"description": "The entry point of the script. Deprecated in favour of reference."
542578
},
543579
"extras": {
544580
"type": "array",

poetry/core/masonry/builders/builder.py

+59-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import shutil
44
import sys
55
import tempfile
6+
import warnings
67

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

286287
# Scripts -> Entry points
287288
for name, ep in self._poetry.local_config.get("scripts", {}).items():
288-
extras = ""
289-
if isinstance(ep, dict):
289+
extras: str = ""
290+
module_path: str = ""
291+
292+
# Currently we support 2 legacy and 1 new format:
293+
# (legacy) my_script = 'my_package.main:entry'
294+
# (legacy) my_script = { callable = 'my_package.main:entry' }
295+
# (supported) my_script = { reference = 'my_package.main:entry', type = "console" }
296+
297+
if isinstance(ep, str):
298+
warnings.warn(
299+
"This way of declaring console scripts is deprecated and will be removed in a future version. "
300+
'Use reference = "{}", type = "console" instead.'.format(ep),
301+
DeprecationWarning,
302+
)
303+
extras = ""
304+
module_path = ep
305+
elif isinstance(ep, dict) and (
306+
ep.get("type") == "console"
307+
or "callable" in ep # Supporting both new and legacy format for now
308+
):
309+
if "callable" in ep:
310+
warnings.warn(
311+
"Using the keyword callable is deprecated and will be removed in a future version. "
312+
'Use reference = "{}", type = "console" instead.'.format(
313+
ep["callable"]
314+
),
315+
DeprecationWarning,
316+
)
317+
290318
extras = "[{}]".format(", ".join(ep["extras"]))
291-
ep = ep["callable"]
319+
module_path = ep.get("reference", ep.get("callable"))
320+
else:
321+
continue
292322

293-
result["console_scripts"].append("{} = {}{}".format(name, ep, extras))
323+
result["console_scripts"].append(
324+
"{} = {}{}".format(name, module_path, extras)
325+
)
294326

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

304336
return dict(result)
305337

338+
def convert_script_files(self) -> List[Path]:
339+
script_files: List[Path] = []
340+
341+
for _, ep in self._poetry.local_config.get("scripts", {}).items():
342+
if isinstance(ep, dict) and ep.get("type") == "file":
343+
source = ep["reference"]
344+
345+
if Path(source).is_absolute():
346+
raise RuntimeError(
347+
"{} is an absolute path. Expected relative path.".format(source)
348+
)
349+
350+
abs_path = Path.joinpath(self._path, source)
351+
352+
if not abs_path.exists():
353+
raise RuntimeError("{} file-script is not found.".format(abs_path))
354+
if not abs_path.is_file():
355+
raise RuntimeError("{} file-script is not a file.".format(abs_path))
356+
357+
script_files.append(abs_path)
358+
359+
return script_files
360+
306361
@classmethod
307362
def convert_author(cls, author: str) -> Dict[str, str]:
308363
m = AUTHOR_REGEX.match(author)

poetry/core/masonry/builders/sdist.py

+9
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ def build_setup(self) -> bytes:
188188
before.append("entry_points = \\\n{}\n".format(pformat(entry_points)))
189189
extra.append("'entry_points': entry_points,")
190190

191+
script_files = self.convert_script_files()
192+
if script_files:
193+
rel_paths = [str(p.relative_to(self._path)) for p in script_files]
194+
before.append('scripts = \\\n["{}"]\n'.format('", "'.join(rel_paths)))
195+
extra.append("'scripts': scripts,")
196+
191197
if self._package.python_versions != "*":
192198
python_requires = self._meta.requires_python
193199

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

323+
# add script files
324+
additional_files.update(self.convert_script_files())
325+
317326
# Include project files
318327
additional_files.add("pyproject.toml")
319328

poetry/core/masonry/builders/wheel.py

+15
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def build(self) -> None:
107107
self._copy_module(zip_file)
108108
self._build(zip_file)
109109

110+
self._copy_file_scripts(zip_file)
110111
self._write_metadata(zip_file)
111112
self._write_record(zip_file)
112113

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

165166
self._add_file(wheel, pkg, rel_path)
166167

168+
def _copy_file_scripts(self, wheel: zipfile.ZipFile) -> None:
169+
file_scripts = self.convert_script_files()
170+
171+
for abs_path in file_scripts:
172+
self._add_file(
173+
wheel,
174+
abs_path,
175+
Path.joinpath(Path(self.wheel_data_folder), "scripts", abs_path.name),
176+
)
177+
167178
def _run_build_command(self, setup: Path) -> None:
168179
subprocess.check_call(
169180
[
@@ -238,6 +249,10 @@ def _write_record(self, wheel: zipfile.ZipFile) -> None:
238249
def dist_info(self) -> str:
239250
return self.dist_info_name(self._package.name, self._meta.version)
240251

252+
@property
253+
def wheel_data_folder(self) -> str:
254+
return "{}-{}.data".format(self._package.name, self._meta.version)
255+
241256
@property
242257
def wheel_filename(self) -> str:
243258
return "{}-{}-{}.whl".format(

tests/fixtures/complete.toml

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pytest-cov = "^2.4"
3838

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

4244

4345
[[tool.poetry.source]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env python
2+
3+
hello = "Hello World!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
echo "Hello World!"

tests/masonry/builders/fixtures/case_sensitive_exclusions/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ time = ["pendulum"]
4646
[tool.poetry.scripts]
4747
my-script = "my_package:main"
4848
my-2nd-script = "my_package:main2"
49-
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
49+
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
echo "Hello World!"

tests/masonry/builders/fixtures/complete/pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ time = ["pendulum"]
4848
[tool.poetry.scripts]
4949
my-script = "my_package:main"
5050
my-2nd-script = "my_package:main2"
51-
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
51+
extra-script-legacy = {callable = "my_package.extra_legacy:main", extras = ["time"]}
52+
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}
53+
sh-script = {reference = "bin/script1.sh", type = "file"}
54+
5255

5356
[tool.poetry.urls]
5457
"Issue Tracker" = "https://github.com/python-poetry/poetry/issues"

tests/masonry/builders/fixtures/invalid_case_sensitive_exclusions/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ time = ["pendulum"]
4141
[tool.poetry.scripts]
4242
my-script = "my_package:main"
4343
my-2nd-script = "my_package:main2"
44-
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
44+
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}

tests/masonry/builders/fixtures/licenses_and_copying/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ time = ["pendulum"]
4343
[tool.poetry.scripts]
4444
my-script = "my_package:main"
4545
my-2nd-script = "my_package:main2"
46-
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
46+
extra-script = {reference = "my_package.extra:main", extras = ["time"], type = "console"}
4747

4848
[tool.poetry.urls]
4949
"Issue Tracker" = "https://github.com/python-poetry/poetry/issues"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Missing Script Files
2+
========

tests/masonry/builders/fixtures/missing_script_files/missing_script_files/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[tool.poetry]
2+
name = "missing-script-files"
3+
version = "0.1"
4+
description = "Some description."
5+
authors = [
6+
"Sébastien Eustace <[email protected]>"
7+
]
8+
readme = "README.rst"
9+
10+
[tool.poetry.scripts]
11+
missing_file = {reference = "not_existing_folder/not_existing_file.sh", type = "file"}
12+
13+
14+
[tool.poetry.dependencies]
15+
python = "3.6"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Script File Invalid Definition
2+
========
3+
4+
This is a use case where the user provides a pyproject.toml where the file script definition is wrong.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
echo "Hello World"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[tool.poetry]
2+
name = "script_file_invalid_definition"
3+
version = "0.1"
4+
description = "Some description."
5+
authors = [
6+
"Sébastien Eustace <[email protected]>"
7+
]
8+
readme = "README.rst"
9+
10+
[tool.poetry.scripts]
11+
invalid_definition = {reference = "bin/script.sh", type = "ffiillee"}
12+
13+
14+
[tool.poetry.dependencies]
15+
python = "3.6"

tests/masonry/builders/fixtures/script_file_invalid_definition/script_file_invalid_definition/__init__.py

Whitespace-only changes.

tests/masonry/builders/test_builder.py

+25
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,28 @@ def test_metadata_with_url_dependencies():
154154
"demo @ https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"
155155
== requires_dist
156156
)
157+
158+
159+
def test_missing_script_files_throws_error():
160+
builder = Builder(
161+
Factory().create_poetry(
162+
Path(__file__).parent / "fixtures" / "missing_script_files"
163+
)
164+
)
165+
166+
with pytest.raises(RuntimeError) as err:
167+
builder.convert_script_files()
168+
169+
assert "file-script is not found." in err.value.args[0]
170+
171+
172+
def test_invalid_script_files_definition():
173+
with pytest.raises(RuntimeError) as err:
174+
Builder(
175+
Factory().create_poetry(
176+
Path(__file__).parent / "fixtures" / "script_file_invalid_definition"
177+
)
178+
)
179+
180+
assert "configuration is invalid" in err.value.args[0]
181+
assert "[scripts.invalid_definition]" in err.value.args[0]

0 commit comments

Comments
 (0)