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")), ] )