Skip to content
Closed
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
8 changes: 8 additions & 0 deletions docs/docs/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 18 additions & 10 deletions poetry/masonry/builders/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -91,14 +90,18 @@ def find_files_to_add(self, exclude_build=True): # type: () -> list
" - Adding: <comment>{}</comment>".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))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail with newer pip which creates and installs a wheel from the source distribution and supports pyproject.toml. Since pyproject.toml still refers to the original path of the script, it will fail since those would not exist in the sdist.

One approach to fix this may be to include the original file with the original path in the source distribution and then in the generated setup.py we can copy those files to a temporary directory with the correct name (since setuptools does not provide a facility for mapping to a different name in the scripts key) and then pass those paths in the scripts list to setup().

Copy link
Copy Markdown

@chrahunt chrahunt May 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also using relative_to like below would be good since it would reject values like "../other-project/bin/script". convert_entry_points will need to return absolute paths for scripts in order for this to behave as expected.


# Include project files
self._io.writeln(
" - Adding: <comment>pyproject.toml</comment>",
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*"):
Expand All @@ -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
Expand All @@ -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():
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec might make more sense vs ep since it can be an "entry point" or a script

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", {})
Expand All @@ -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
Expand Down
13 changes: 10 additions & 3 deletions poetry/masonry/builders/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
27 changes: 21 additions & 6 deletions poetry/masonry/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 + "*")):
Expand Down Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The directory itself is *.data, but naming the property data would probably be confusing. What do you think about naming the methods below make_data_name and make_dist_info_name and then naming this method data_name and changing dist_info to dist_info_name?

return self.data_info_name(self._package.name, self._meta.version)

@property
def wheel_filename(self): # type: () -> str
return "{}-{}-{}.whl".format(
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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))
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions poetry/masonry/utils/helpers.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Empty file.
1 change: 1 addition & 0 deletions tests/masonry/builders/fixtures/complete/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
27 changes: 19 additions & 8 deletions tests/masonry/builders/test_sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}


Expand Down Expand Up @@ -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")),
]
)

Expand Down