diff --git a/docs/docs/pyproject.md b/docs/docs/pyproject.md
index 2ff6317a36b..9a20474cb8c 100644
--- a/docs/docs/pyproject.md
+++ b/docs/docs/pyproject.md
@@ -173,6 +173,14 @@ poetry = 'poetry:console.run'
Here, we will have the `poetry` script installed which will execute `console.run` in the `poetry` package.
+Poetry also supports scripts which are not necessarily Python functions. To include these in your package,
+provide the path to the script:
+
+```toml
+[tool.poetry.scripts]
+my-script = {path = "path/to/script"}
+```
+
## `extras`
Poetry supports extras to allow expression of:
diff --git a/poetry/masonry/builders/builder.py b/poetry/masonry/builders/builder.py
index b1331ac9e00..3d062f7851b 100644
--- a/poetry/masonry/builders/builder.py
+++ b/poetry/masonry/builders/builder.py
@@ -68,7 +68,6 @@ def find_files_to_add(self, exclude_build=True): # type: () -> list
Finds all files to add to the tarball
"""
excluded = self.find_excluded_files()
- src = self._module.path
to_add = []
for include in self._module.includes:
@@ -91,14 +90,18 @@ def find_files_to_add(self, exclude_build=True): # type: () -> list
" - Adding: {}".format(str(file)),
verbosity=self._io.VERBOSITY_VERY_VERBOSE,
)
- to_add.append(file)
+ to_add.append((file, file))
+
+ _, scripts = self.convert_entry_points()
+ for name, script in scripts.items():
+ to_add.append((Path(script), Path("bin") / name))
# Include project files
self._io.writeln(
" - Adding: pyproject.toml",
verbosity=self._io.VERBOSITY_VERY_VERBOSE,
)
- to_add.append(Path("pyproject.toml"))
+ to_add.append((Path("pyproject.toml"), Path("pyproject.toml")))
# If a license file exists, add it
for license_file in self._path.glob("LICENSE*"):
@@ -108,7 +111,8 @@ def find_files_to_add(self, exclude_build=True): # type: () -> list
),
verbosity=self._io.VERBOSITY_VERY_VERBOSE,
)
- to_add.append(license_file.relative_to(self._path))
+ file_path = license_file.relative_to(self._path)
+ to_add.append((file_path, file_path))
# If a README is specificed we need to include it
# to avoid errors
@@ -121,21 +125,26 @@ def find_files_to_add(self, exclude_build=True): # type: () -> list
),
verbosity=self._io.VERBOSITY_VERY_VERBOSE,
)
- to_add.append(readme.relative_to(self._path))
+ readme_path = readme.relative_to(self._path)
+ to_add.append((readme_path, readme_path))
# If a build script is specified and explicitely required
# we add it to the list of files
if self._package.build and not exclude_build:
- to_add.append(Path(self._package.build))
+ to_add.append((Path(self._package.build), Path(self._package.build)))
return sorted(to_add)
- def convert_entry_points(self): # type: () -> dict
+ def convert_entry_points(self): # type: () -> (dict, dict)
result = defaultdict(list)
+ scripts = {}
# Scripts -> Entry points
for name, ep in self._poetry.local_config.get("scripts", {}).items():
- result["console_scripts"].append("{} = {}".format(name, ep))
+ if isinstance(ep, dict):
+ scripts[name] = ep["path"]
+ else:
+ result["console_scripts"].append("{} = {}".format(name, ep))
# Plugins -> entry points
plugins = self._poetry.local_config.get("plugins", {})
@@ -145,8 +154,7 @@ def convert_entry_points(self): # type: () -> dict
for groupname in result:
result[groupname] = sorted(result[groupname])
-
- return dict(result)
+ return dict(result), scripts
@classmethod
def convert_author(cls, author): # type: () -> dict
diff --git a/poetry/masonry/builders/sdist.py b/poetry/masonry/builders/sdist.py
index 45be408bbef..d6988a173d6 100644
--- a/poetry/masonry/builders/sdist.py
+++ b/poetry/masonry/builders/sdist.py
@@ -76,10 +76,10 @@ def build(self, target_dir=None): # type: (Path) -> Path
files_to_add = self.find_files_to_add(exclude_build=False)
- for relpath in files_to_add:
+ for relpath, targetpath in files_to_add:
path = self._path / relpath
tar_info = tar.gettarinfo(
- str(path), arcname=pjoin(tar_dir, str(relpath))
+ str(path), arcname=pjoin(tar_dir, str(targetpath))
)
tar_info = self.clean_tarinfo(tar_info)
@@ -168,10 +168,17 @@ def build_setup(self): # type: () -> bytes
before.append("extras_require = \\\n{}\n".format(pformat(extras)))
extra.append("'extras_require': extras_require,")
- entry_points = self.convert_entry_points()
+ entry_points, scripts = self.convert_entry_points()
if entry_points:
before.append("entry_points = \\\n{}\n".format(pformat(entry_points)))
extra.append("'entry_points': entry_points,")
+ if scripts:
+ before.append(
+ "scripts = \\\n{}\n".format(
+ pformat(["bin/{name}".format(name=name) for name in scripts])
+ )
+ )
+ extra.append("'scripts': scripts,")
if self._package.python_versions != "*":
python_requires = self._meta.requires_python
diff --git a/poetry/masonry/builders/wheel.py b/poetry/masonry/builders/wheel.py
index d920294bfb4..2f9e198d728 100644
--- a/poetry/masonry/builders/wheel.py
+++ b/poetry/masonry/builders/wheel.py
@@ -107,7 +107,6 @@ def _build(self):
def _copy_module(self, wheel):
excluded = self.find_excluded_files()
- src = self._module.path
to_add = []
for include in self._module.includes:
@@ -148,7 +147,7 @@ def _write_metadata(self, wheel):
or "plugins" in self._poetry.local_config
):
with self._write_to_zip(wheel, self.dist_info + "/entry_points.txt") as f:
- self._write_entry_points(f)
+ self._write_entry_points(wheel, f)
for base in ("COPYING", "LICENSE"):
for path in sorted(self._path.glob(base + "*")):
@@ -176,6 +175,10 @@ def find_excluded_files(self): # type: () -> list
def dist_info(self): # type: () -> str
return self.dist_info_name(self._package.name, self._meta.version)
+ @property
+ def data_info(self): # type: () -> str
+ return self.data_info_name(self._package.name, self._meta.version)
+
@property
def wheel_filename(self): # type: () -> str
return "{}-{}-{}.whl".format(
@@ -195,6 +198,12 @@ def dist_info_name(self, distribution, version): # type: (...) -> str
return "{}-{}.dist-info".format(escaped_name, escaped_version)
+ def data_info_name(self, distribution, version): # type: (...) -> str
+ escaped_name = re.sub("[^\w\d.]+", "_", distribution, flags=re.UNICODE)
+ escaped_version = re.sub("[^\w\d.]+", "_", version, flags=re.UNICODE)
+
+ return "{}-{}.data".format(escaped_name, escaped_version)
+
@property
def tag(self):
if self._package.build:
@@ -215,7 +224,7 @@ def tag(self):
return "-".join(tag)
- def _add_file(self, wheel, full_path, rel_path):
+ def _add_file(self, wheel, full_path, rel_path, executable=False):
full_path, rel_path = str(full_path), str(rel_path)
if os.sep != "/":
# We always want to have /-separated paths in the zip file and in
@@ -226,7 +235,7 @@ def _add_file(self, wheel, full_path, rel_path):
# Normalize permission bits to either 755 (executable) or 644
st_mode = os.stat(full_path).st_mode
- new_mode = normalize_file_permissions(st_mode)
+ new_mode = normalize_file_permissions(st_mode, executable=executable)
zinfo.external_attr = (new_mode & 0xFFFF) << 16 # Unix attributes
if stat.S_ISDIR(st_mode):
@@ -265,11 +274,11 @@ def _write_to_zip(self, wheel, rel_path):
wheel.writestr(zi, b, compress_type=zipfile.ZIP_DEFLATED)
self._records.append((rel_path, hash_digest, len(b)))
- def _write_entry_points(self, fp):
+ def _write_entry_points(self, wheel, fp):
"""
Write entry_points.txt.
"""
- entry_points = self.convert_entry_points()
+ entry_points, scripts = self.convert_entry_points()
for group_name in sorted(entry_points):
fp.write("[{}]\n".format(group_name))
@@ -278,6 +287,12 @@ def _write_entry_points(self, fp):
fp.write("\n")
+ for name, script in scripts.items():
+ full_path = self._original_path / script
+ self._add_file(
+ wheel, full_path, self.data_info + "/scripts/" + name, executable=True
+ )
+
def _write_wheel_file(self, fp):
fp.write(
wheel_file_template.format(
diff --git a/poetry/masonry/utils/helpers.py b/poetry/masonry/utils/helpers.py
index 2455bb811ae..bf835bf47ee 100644
--- a/poetry/masonry/utils/helpers.py
+++ b/poetry/masonry/utils/helpers.py
@@ -1,4 +1,4 @@
-def normalize_file_permissions(st_mode):
+def normalize_file_permissions(st_mode, executable=False):
"""
Normalizes the permission bits in the st_mode field from stat to 644/755
@@ -8,7 +8,7 @@ def normalize_file_permissions(st_mode):
"""
# Set 644 permissions, leaving higher bits of st_mode unchanged
new_mode = (st_mode | 0o644) & ~0o133
- if st_mode & 0o100:
+ if st_mode & 0o100 or executable:
new_mode |= 0o111 # Executable: 644 -> 755
return new_mode
diff --git a/tests/masonry/builders/fixtures/complete/bin/my-special-script b/tests/masonry/builders/fixtures/complete/bin/my-special-script
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/masonry/builders/fixtures/complete/pyproject.toml b/tests/masonry/builders/fixtures/complete/pyproject.toml
index 87a519305d8..b3edfc3c6de 100644
--- a/tests/masonry/builders/fixtures/complete/pyproject.toml
+++ b/tests/masonry/builders/fixtures/complete/pyproject.toml
@@ -37,3 +37,4 @@ time = ["pendulum"]
[tool.poetry.scripts]
my-script = "my_package:main"
my-2nd-script = "my_package:main2"
+different-name = {path = "bin/my-special-script"}
diff --git a/tests/masonry/builders/test_sdist.py b/tests/masonry/builders/test_sdist.py
index 1b4c1ed7f14..2468de75081 100644
--- a/tests/masonry/builders/test_sdist.py
+++ b/tests/masonry/builders/test_sdist.py
@@ -125,6 +125,7 @@ def test_make_setup():
"my-script = my_package:main",
]
}
+ assert ns["scripts"] == ["bin/different-name"]
assert ns["extras_require"] == {"time": ["pendulum>=1.4,<2.0"]}
@@ -185,14 +186,24 @@ def test_find_files_to_add():
assert sorted(result) == sorted(
[
- Path("LICENSE"),
- Path("README.rst"),
- Path("my_package/__init__.py"),
- Path("my_package/data1/test.json"),
- Path("my_package/sub_pkg1/__init__.py"),
- Path("my_package/sub_pkg2/__init__.py"),
- Path("my_package/sub_pkg2/data2/data.json"),
- Path("pyproject.toml"),
+ (Path("LICENSE"), Path("LICENSE")),
+ (Path("README.rst"), Path("README.rst")),
+ (Path("bin/my-special-script"), Path("bin/different-name")),
+ (Path("my_package/__init__.py"), Path("my_package/__init__.py")),
+ (Path("my_package/data1/test.json"), Path("my_package/data1/test.json")),
+ (
+ Path("my_package/sub_pkg1/__init__.py"),
+ Path("my_package/sub_pkg1/__init__.py"),
+ ),
+ (
+ Path("my_package/sub_pkg2/__init__.py"),
+ Path("my_package/sub_pkg2/__init__.py"),
+ ),
+ (
+ Path("my_package/sub_pkg2/data2/data.json"),
+ Path("my_package/sub_pkg2/data2/data.json"),
+ ),
+ (Path("pyproject.toml"), Path("pyproject.toml")),
]
)