Skip to content

Commit e7ffa72

Browse files
Added script file feature
1 parent abe9fe5 commit e7ffa72

File tree

20 files changed

+205
-8
lines changed

20 files changed

+205
-8
lines changed

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

+44-8
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,7 @@
163163
"scripts": {
164164
"type": "object",
165165
"description": "A hash of scripts to be installed.",
166-
"items": {
167-
"type": "string"
168-
}
166+
"$ref": "#/definitions/scripts"
169167
},
170168
"plugins": {
171169
"type": "object",
@@ -212,13 +210,23 @@
212210
},
213211
"package-format": {
214212
"type": "string",
215-
"enum": ["sdist", "wheel"],
213+
"enum": [
214+
"sdist",
215+
"wheel"
216+
],
216217
"description": "A Python packaging format."
217218
},
218219
"package-formats": {
219220
"oneOf": [
220-
{"$ref": "#/definitions/package-format"},
221-
{"type": "array", "items": {"$ref": "#/definitions/package-format"}}
221+
{
222+
"$ref": "#/definitions/package-format"
223+
},
224+
{
225+
"type": "array",
226+
"items": {
227+
"$ref": "#/definitions/package-format"
228+
}
229+
}
222230
],
223231
"description": "The format(s) for which the package must be included."
224232
},
@@ -513,6 +521,9 @@
513521
},
514522
{
515523
"$ref": "#/definitions/extra-script"
524+
},
525+
{
526+
"$ref": "#/definitions/file-script"
516527
}
517528
]
518529
}
@@ -522,6 +533,27 @@
522533
"type": "string",
523534
"description": "A simple script pointing to a callable object."
524535
},
536+
"file-script": {
537+
"type": "object",
538+
"description": "A script file that'll be included in the distribution package.",
539+
"additionalProperties": false,
540+
"properties": {
541+
"source": {
542+
"type": "string",
543+
"description": "The relative path of the script file."
544+
},
545+
"type": {
546+
"description": "This value can only be 'file' currently.",
547+
"type": "string",
548+
"enum": [
549+
"file"
550+
]
551+
}
552+
},
553+
"required": [
554+
"source"
555+
]
556+
},
525557
"extra-script": {
526558
"type": "object",
527559
"description": "A script that should be installed only if extras are activated.",
@@ -583,8 +615,12 @@
583615
},
584616
"build-section": {
585617
"oneOf": [
586-
{"$ref": "#/definitions/build-script"},
587-
{"$ref": "#/definitions/build-config"}
618+
{
619+
"$ref": "#/definitions/build-script"
620+
},
621+
{
622+
"$ref": "#/definitions/build-config"
623+
}
588624
]
589625
}
590626
}

poetry/core/masonry/builders/builder.py

+19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections import defaultdict
99
from contextlib import contextmanager
1010
from typing import TYPE_CHECKING
11+
from typing import List
1112
from typing import Optional
1213
from typing import Set
1314
from typing import Union
@@ -271,6 +272,8 @@ def convert_entry_points(self): # type: () -> dict
271272
for name, ep in self._poetry.local_config.get("scripts", {}).items():
272273
extras = ""
273274
if isinstance(ep, dict):
275+
if "source" in ep:
276+
continue
274277
extras = "[{}]".format(", ".join(ep["extras"]))
275278
ep = ep["callable"]
276279

@@ -287,6 +290,22 @@ def convert_entry_points(self): # type: () -> dict
287290

288291
return dict(result)
289292

293+
def convert_script_files(self): # type: () -> List[Path]
294+
script_files = []
295+
296+
for _, ep in self._poetry.local_config.get("scripts", {}).items():
297+
if isinstance(ep, dict) and "source" in ep:
298+
abs_path = Path.joinpath(self._path, ep["source"])
299+
300+
if not abs_path.exists():
301+
raise RuntimeError("{} file-script is not found.".format(abs_path))
302+
if not abs_path.is_file():
303+
raise RuntimeError("{} file-script is not a file.".format(abs_path))
304+
305+
script_files.append(abs_path)
306+
307+
return script_files
308+
290309
@classmethod
291310
def convert_author(cls, author): # type: (...) -> dict
292311
m = AUTHOR_REGEX.match(author)

poetry/core/masonry/builders/sdist.py

+10
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ def build_setup(self): # type: () -> bytes
181181
before.append("entry_points = \\\n{}\n".format(pformat(entry_points)))
182182
extra.append("'entry_points': entry_points,")
183183

184+
script_files = self.convert_script_files()
185+
if script_files:
186+
rel_paths = [str(p.relative_to(self._path)) for p in script_files]
187+
before.append('scripts = \\\n["{}"]\n'.format('", "'.join(rel_paths)))
188+
extra.append("'scripts': scripts,")
189+
184190
if self._package.python_versions != "*":
185191
python_requires = self._meta.requires_python
186192

@@ -311,6 +317,10 @@ def find_files_to_add(
311317
license_file for license_file in self._path.glob("LICENSE*")
312318
}
313319

320+
# add script files
321+
for abs_path in self.convert_script_files():
322+
additional_files.add(abs_path)
323+
314324
# Include project files
315325
additional_files.add("pyproject.toml")
316326

poetry/core/masonry/builders/wheel.py

+15
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def build(self):
8888
self._copy_module(zip_file)
8989
self._build(zip_file)
9090

91+
self._copy_file_scripts(zip_file)
9192
self._write_metadata(zip_file)
9293
self._write_record(zip_file)
9394

@@ -145,6 +146,16 @@ def _build(self, wheel):
145146

146147
self._add_file(wheel, pkg, rel_path)
147148

149+
def _copy_file_scripts(self, wheel): # type: (zipfile.ZipFile) -> None
150+
file_scripts = self.convert_script_files()
151+
152+
for abs_path in file_scripts:
153+
self._add_file(
154+
wheel,
155+
abs_path,
156+
"{}/{}/{}".format(self.wheel_data_folder, "scripts", abs_path.name),
157+
)
158+
148159
def _run_build_command(self, setup):
149160
subprocess.check_call(
150161
[
@@ -219,6 +230,10 @@ def _write_record(self, wheel):
219230
def dist_info(self): # type: () -> str
220231
return self.dist_info_name(self._package.name, self._meta.version)
221232

233+
@property
234+
def wheel_data_folder(self): # type: () -> str
235+
return "{}-{}.data".format(self._package.name, self._meta.version)
236+
222237
@property
223238
def wheel_filename(self): # type: () -> str
224239
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 = { source = "script-files/sample_script.py", type= "file" }
42+
sample_shscript = { source = "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!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
echo "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/complete/pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ time = ["pendulum"]
4949
my-script = "my_package:main"
5050
my-2nd-script = "my_package:main2"
5151
extra-script = {callable = "my_package.extra:main", extras = ["time"]}
52+
sh-script = {source = "bin/script1.sh", type = "file"}
53+
sh-script2 = {source = "bin/script2.sh" }
54+
5255

5356
[tool.poetry.urls]
5457
"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 = {source = "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 = {source = "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

+26
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,29 @@ 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]
182+

tests/masonry/builders/test_complete.py

+35
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,16 @@ def test_complete():
224224

225225
try:
226226
assert "my_package/sub_pgk1/extra_file.xml" not in zip.namelist()
227+
assert "my-package-1.2.3.data/scripts/script1.sh" in zip.namelist()
228+
assert "my-package-1.2.3.data/scripts/script2.sh" in zip.namelist()
229+
assert (
230+
"Hello World"
231+
in zip.read("my-package-1.2.3.data/scripts/script1.sh").decode()
232+
)
233+
assert (
234+
"Hello World"
235+
in zip.read("my-package-1.2.3.data/scripts/script2.sh").decode()
236+
)
227237

228238
entry_points = zip.read("my_package-1.2.3.dist-info/entry_points.txt")
229239

@@ -289,6 +299,29 @@ def test_complete():
289299
290300
"""
291301
)
302+
actual_records = decode(zip.read("my_package-1.2.3.dist-info/RECORD"))
303+
304+
# For some reason, the ordering of the files and the SHA hashes
305+
# vary per operating systems and Python versions.
306+
# So instead of 1:1 assertion, let's do a bit clunkier one:
307+
308+
expected_records = [
309+
"my_package/__init__.py",
310+
"my_package/data1/test.json",
311+
"my_package/sub_pkg1/__init__.py",
312+
"my_package/sub_pkg2/__init__.py",
313+
"my_package/sub_pkg2/data2/data.json",
314+
"my-package-1.2.3.data/scripts/script1.sh",
315+
"my-package-1.2.3.data/scripts/script2.sh",
316+
"my_package-1.2.3.dist-info/entry_points.txt",
317+
"my_package-1.2.3.dist-info/LICENSE",
318+
"my_package-1.2.3.dist-info/WHEEL",
319+
"my_package-1.2.3.dist-info/METADATA",
320+
]
321+
322+
for expected_record in expected_records:
323+
assert expected_record in actual_records
324+
292325
finally:
293326
zip.close()
294327

@@ -317,6 +350,8 @@ def test_complete_no_vcs():
317350
"my_package/sub_pkg1/__init__.py",
318351
"my_package/sub_pkg2/__init__.py",
319352
"my_package/sub_pkg2/data2/data.json",
353+
"my-package-1.2.3.data/scripts/script1.sh",
354+
"my-package-1.2.3.data/scripts/script2.sh",
320355
"my_package/sub_pkg3/foo.py",
321356
"my_package-1.2.3.dist-info/entry_points.txt",
322357
"my_package-1.2.3.dist-info/LICENSE",

tests/masonry/builders/test_sdist.py

+2
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ def test_find_files_to_add():
171171
[
172172
Path("LICENSE"),
173173
Path("README.rst"),
174+
Path("bin/script1.sh"),
175+
Path("bin/script2.sh"),
174176
Path("my_package/__init__.py"),
175177
Path("my_package/data1/test.json"),
176178
Path("my_package/sub_pkg1/__init__.py"),

0 commit comments

Comments
 (0)