editable.py (poetry-1.1.15) | : | editable.py (poetry-1.2.0) | ||
---|---|---|---|---|
from __future__ import unicode_literals | from __future__ import annotations | |||
import csv | ||||
import hashlib | import hashlib | |||
import json | ||||
import os | import os | |||
import shutil | import shutil | |||
from base64 import urlsafe_b64encode | from base64 import urlsafe_b64encode | |||
from pathlib import Path | ||||
from typing import TYPE_CHECKING | ||||
from poetry.core.masonry.builders.builder import Builder | from poetry.core.masonry.builders.builder import Builder | |||
from poetry.core.masonry.builders.sdist import SdistBuilder | from poetry.core.masonry.builders.sdist import SdistBuilder | |||
from poetry.core.masonry.utils.package_include import PackageInclude | from poetry.core.masonry.utils.package_include import PackageInclude | |||
from poetry.core.semver.version import Version | from poetry.core.semver.version import Version | |||
from poetry.utils._compat import WINDOWS | from poetry.utils._compat import WINDOWS | |||
from poetry.utils._compat import Path | ||||
from poetry.utils._compat import decode | from poetry.utils._compat import decode | |||
from poetry.utils.env import build_environment | ||||
from poetry.utils.helpers import is_dir_writable | from poetry.utils.helpers import is_dir_writable | |||
from poetry.utils.pip import pip_install | ||||
if TYPE_CHECKING: | ||||
from cleo.io.io import IO | ||||
from poetry.poetry import Poetry | ||||
from poetry.utils.env import Env | ||||
SCRIPT_TEMPLATE = """\ | SCRIPT_TEMPLATE = """\ | |||
#!{python} | #!{python} | |||
import sys | ||||
from {module} import {callable_holder} | from {module} import {callable_holder} | |||
if __name__ == '__main__': | if __name__ == '__main__': | |||
{callable_}() | sys.exit({callable_}()) | |||
""" | """ | |||
WINDOWS_CMD_TEMPLATE = """\ | WINDOWS_CMD_TEMPLATE = """\ | |||
@echo off\r\n"{python}" "%~dp0\\{script}" %*\r\n | @echo off\r\n"{python}" "%~dp0\\{script}" %*\r\n | |||
""" | """ | |||
class EditableBuilder(Builder): | class EditableBuilder(Builder): | |||
def __init__(self, poetry, env, io): | def __init__(self, poetry: Poetry, env: Env, io: IO) -> None: | |||
super(EditableBuilder, self).__init__(poetry) | super().__init__(poetry) | |||
self._env = env | self._env = env | |||
self._io = io | self._io = io | |||
def build(self): | def build(self, target_dir: Path | None = None) -> Path: | |||
self._debug( | self._debug( | |||
" - Building package <c1>{}</c1> in <info>editable</info> mode".for | f" - Building package <c1>{self._package.name}</c1> in" | |||
mat( | " <info>editable</info> mode" | |||
self._package.name | ||||
) | ||||
) | ) | |||
if self._package.build_script: | if self._package.build_script: | |||
if self._package.build_should_generate_setup(): | if self._package.build_should_generate_setup(): | |||
self._debug( | self._debug( | |||
" - <warning>Falling back on using a <b>setup.py</b></warni ng>" | " - <warning>Falling back on using a <b>setup.py</b></warni ng>" | |||
) | ) | |||
self._setup_build() | ||||
return self._setup_build() | path: Path = self._path | |||
return path | ||||
self._run_build_script(self._package.build_script) | self._run_build_script(self._package.build_script) | |||
for removed in self._env.site_packages.remove_distribution_files( | ||||
distribution_name=self._package.name | ||||
): | ||||
self._debug( | ||||
f" - Removed <c2>{removed.name}</c2> directory from" | ||||
f" <b>{removed.parent}</b>" | ||||
) | ||||
added_files = [] | added_files = [] | |||
added_files += self._add_pth() | added_files += self._add_pth() | |||
added_files += self._add_scripts() | added_files += self._add_scripts() | |||
self._add_dist_info(added_files) | self._add_dist_info(added_files) | |||
def _run_build_script(self, build_script): | path = self._path | |||
self._debug(" - Executing build script: <b>{}</b>".format(build_script) | return path | |||
) | ||||
self._env.run("python", str(self._path.joinpath(build_script)), call=Tru | def _run_build_script(self, build_script: str) -> None: | |||
e) | with build_environment(poetry=self._poetry, env=self._env, io=self._io) | |||
as env: | ||||
self._debug(f" - Executing build script: <b>{build_script}</b>") | ||||
env.run("python", str(self._path.joinpath(build_script)), call=True) | ||||
def _setup_build(self): | def _setup_build(self) -> None: | |||
builder = SdistBuilder(self._poetry) | builder = SdistBuilder(self._poetry) | |||
setup = self._path / "setup.py" | setup = self._path / "setup.py" | |||
has_setup = setup.exists() | has_setup = setup.exists() | |||
if has_setup: | if has_setup: | |||
self._io.write_line( | self._io.write_error_line( | |||
"<warning>A setup.py file already exists. Using it.</warning>" | "<warning>A setup.py file already exists. Using it.</warning>" | |||
) | ) | |||
else: | else: | |||
with setup.open("w", encoding="utf-8") as f: | with setup.open("w", encoding="utf-8") as f: | |||
f.write(decode(builder.build_setup())) | f.write(decode(builder.build_setup())) | |||
try: | try: | |||
if self._env.pip_version < Version(19, 0): | if self._env.pip_version < Version.from_parts(19, 0): | |||
self._env.run_pip("install", "-e", str(self._path), "--no-deps") | pip_install(self._path, self._env, upgrade=True, editable=True) | |||
else: | else: | |||
# Temporarily rename pyproject.toml | # Temporarily rename pyproject.toml | |||
shutil.move( | shutil.move( | |||
str(self._poetry.file), str(self._poetry.file.with_suffix(". tmp")) | str(self._poetry.file), str(self._poetry.file.with_suffix(". tmp")) | |||
) | ) | |||
try: | try: | |||
self._env.run_pip("install", "-e", str(self._path), "--no-de ps") | pip_install(self._path, self._env, upgrade=True, editable=Tr ue) | |||
finally: | finally: | |||
shutil.move( | shutil.move( | |||
str(self._poetry.file.with_suffix(".tmp")), | str(self._poetry.file.with_suffix(".tmp")), | |||
str(self._poetry.file), | str(self._poetry.file), | |||
) | ) | |||
finally: | finally: | |||
if not has_setup: | if not has_setup: | |||
os.remove(str(setup)) | os.remove(str(setup)) | |||
def _add_pth(self): | def _add_pth(self) -> list[Path]: | |||
paths = set() | paths = { | |||
for include in self._module.includes: | include.base.resolve().as_posix() | |||
if isinstance(include, PackageInclude) and ( | for include in self._module.includes | |||
include.is_module() or include.is_package() | if isinstance(include, PackageInclude) | |||
): | and (include.is_module() or include.is_package()) | |||
paths.add(include.base.resolve().as_posix()) | } | |||
content = "" | ||||
for path in paths: | ||||
content += decode(path + os.linesep) | ||||
content = "".join(decode(path + os.linesep) for path in paths) | ||||
pth_file = Path(self._module.name).with_suffix(".pth") | pth_file = Path(self._module.name).with_suffix(".pth") | |||
# remove any pre-existing pth files for this package | ||||
for file in self._env.site_packages.find(path=pth_file, writable_only=Tr | ||||
ue): | ||||
self._debug( | ||||
f" - Removing existing <c2>{file.name}</c2> from <b>{file.paren | ||||
t}</b>" | ||||
f" for {self._poetry.file.parent}" | ||||
) | ||||
# We can't use unlink(missing_ok=True) because it's not always avail | ||||
able | ||||
if file.exists(): | ||||
file.unlink() | ||||
try: | try: | |||
pth_file = self._env.site_packages.write_text( | pth_file = self._env.site_packages.write_text( | |||
pth_file, content, encoding="utf-8" | pth_file, content, encoding="utf-8" | |||
) | ) | |||
self._debug( | self._debug( | |||
" - Adding <c2>{}</c2> to <b>{}</b> for {}".format( | f" - Adding <c2>{pth_file.name}</c2> to <b>{pth_file.parent}</b | |||
pth_file.name, pth_file.parent, self._poetry.file.parent | > for" | |||
) | f" {self._poetry.file.parent}" | |||
) | ) | |||
return [pth_file] | return [pth_file] | |||
except OSError: | except OSError: | |||
# TODO: Replace with PermissionError | # TODO: Replace with PermissionError | |||
self._io.error_line( | self._io.write_error_line( | |||
" - Failed to create <c2>{}</c2> for {}".format( | f" - Failed to create <c2>{pth_file.name}</c2> for" | |||
pth_file.name, self._poetry.file.parent | f" {self._poetry.file.parent}" | |||
) | ||||
) | ) | |||
return [] | return [] | |||
def _add_scripts(self): | def _add_scripts(self) -> list[Path]: | |||
added = [] | added = [] | |||
entry_points = self.convert_entry_points() | entry_points = self.convert_entry_points() | |||
for scripts_path in self._env.script_dirs: | for scripts_path in self._env.script_dirs: | |||
if is_dir_writable(path=scripts_path, create=True): | if is_dir_writable(path=scripts_path, create=True): | |||
break | break | |||
else: | else: | |||
self._io.error_line( | self._io.write_error_line( | |||
" - Failed to find a suitable script installation directory for | " - Failed to find a suitable script installation directory for | |||
{}".format( | " | |||
self._poetry.file.parent | f" {self._poetry.file.parent}" | |||
) | ||||
) | ) | |||
return [] | return [] | |||
scripts = entry_points.get("console_scripts", []) | scripts = entry_points.get("console_scripts", []) | |||
for script in scripts: | for script in scripts: | |||
name, script = script.split(" = ") | name, script = script.split(" = ") | |||
module, callable_ = script.split(":") | module, callable_ = script.split(":") | |||
callable_holder = callable_.split(".", 1)[0] | callable_holder = callable_.split(".", 1)[0] | |||
script_file = scripts_path.joinpath(name) | script_file = scripts_path.joinpath(name) | |||
self._debug( | self._debug( | |||
" - Adding the <c2>{}</c2> script to <b>{}</b>".format( | f" - Adding the <c2>{name}</c2> script to <b>{scripts_path}</b> | |||
name, scripts_path | " | |||
) | ||||
) | ) | |||
with script_file.open("w", encoding="utf-8") as f: | with script_file.open("w", encoding="utf-8") as f: | |||
f.write( | f.write( | |||
decode( | decode( | |||
SCRIPT_TEMPLATE.format( | SCRIPT_TEMPLATE.format( | |||
python=self._env.python, | python=self._env.python, | |||
module=module, | module=module, | |||
callable_holder=callable_holder, | callable_holder=callable_holder, | |||
callable_=callable_, | callable_=callable_, | |||
) | ) | |||
skipping to change at line 174 | skipping to change at line 202 | |||
) | ) | |||
script_file.chmod(0o755) | script_file.chmod(0o755) | |||
added.append(script_file) | added.append(script_file) | |||
if WINDOWS: | if WINDOWS: | |||
cmd_script = script_file.with_suffix(".cmd") | cmd_script = script_file.with_suffix(".cmd") | |||
cmd = WINDOWS_CMD_TEMPLATE.format(python=self._env.python, scrip t=name) | cmd = WINDOWS_CMD_TEMPLATE.format(python=self._env.python, scrip t=name) | |||
self._debug( | self._debug( | |||
" - Adding the <c2>{}</c2> script wrapper to <b>{}</b>".for | f" - Adding the <c2>{cmd_script.name}</c2> script wrapper t | |||
mat( | o" | |||
cmd_script.name, scripts_path | f" <b>{scripts_path}</b>" | |||
) | ||||
) | ) | |||
with cmd_script.open("w", encoding="utf-8") as f: | with cmd_script.open("w", encoding="utf-8") as f: | |||
f.write(decode(cmd)) | f.write(decode(cmd)) | |||
added.append(cmd_script) | added.append(cmd_script) | |||
return added | return added | |||
def _add_dist_info(self, added_files): | def _add_dist_info(self, added_files: list[Path]) -> None: | |||
from poetry.core.masonry.builders.wheel import WheelBuilder | from poetry.core.masonry.builders.wheel import WheelBuilder | |||
added_files = added_files[:] | added_files = added_files[:] | |||
builder = WheelBuilder(self._poetry) | builder = WheelBuilder(self._poetry) | |||
dist_info = self._env.site_packages.mkdir(Path(builder.dist_info)) | ||||
dist_info_path = Path(builder.dist_info) | ||||
for dist_info in self._env.site_packages.find( | ||||
dist_info_path, writable_only=True | ||||
): | ||||
if dist_info.exists(): | ||||
self._debug( | ||||
" - Removing existing <c2>{}</c2> directory from <b>{}</b>" | ||||
.format( | ||||
dist_info.name, dist_info.parent | ||||
) | ||||
) | ||||
shutil.rmtree(str(dist_info)) | ||||
dist_info = self._env.site_packages.mkdir(dist_info_path) | ||||
self._debug( | self._debug( | |||
" - Adding the <c2>{}</c2> directory to <b>{}</b>".format( | f" - Adding the <c2>{dist_info.name}</c2> directory to" | |||
dist_info.name, dist_info.parent | f" <b>{dist_info.parent}</b>" | |||
) | ||||
) | ) | |||
with dist_info.joinpath("METADATA").open("w", encoding="utf-8") as f: | with dist_info.joinpath("METADATA").open("w", encoding="utf-8") as f: | |||
builder._write_metadata_file(f) | builder._write_metadata_file(f) | |||
added_files.append(dist_info.joinpath("METADATA")) | added_files.append(dist_info.joinpath("METADATA")) | |||
with dist_info.joinpath("INSTALLER").open("w", encoding="utf-8") as f: | with dist_info.joinpath("INSTALLER").open("w", encoding="utf-8") as f: | |||
f.write("poetry") | f.write("poetry") | |||
added_files.append(dist_info.joinpath("INSTALLER")) | added_files.append(dist_info.joinpath("INSTALLER")) | |||
if self.convert_entry_points(): | if self.convert_entry_points(): | |||
with dist_info.joinpath("entry_points.txt").open( | with dist_info.joinpath("entry_points.txt").open( | |||
"w", encoding="utf-8" | "w", encoding="utf-8" | |||
) as f: | ) as f: | |||
builder._write_entry_points(f) | builder._write_entry_points(f) | |||
added_files.append(dist_info.joinpath("entry_points.txt")) | added_files.append(dist_info.joinpath("entry_points.txt")) | |||
with dist_info.joinpath("RECORD").open("w", encoding="utf-8") as f: | # write PEP 610 metadata | |||
direct_url_json = dist_info.joinpath("direct_url.json") | ||||
direct_url_json.write_text( | ||||
json.dumps( | ||||
{ | ||||
"dir_info": {"editable": True}, | ||||
"url": self._poetry.file.path.parent.as_uri(), | ||||
} | ||||
) | ||||
) | ||||
added_files.append(direct_url_json) | ||||
record = dist_info.joinpath("RECORD") | ||||
with record.open("w", encoding="utf-8", newline="") as f: | ||||
csv_writer = csv.writer(f) | ||||
for path in added_files: | for path in added_files: | |||
hash = self._get_file_hash(path) | hash = self._get_file_hash(path) | |||
size = path.stat().st_size | size = path.stat().st_size | |||
f.write("{},sha256={},{}\n".format(str(path), hash, size)) | csv_writer.writerow((path, f"sha256={hash}", size)) | |||
# RECORD itself is recorded with no hash or size | # RECORD itself is recorded with no hash or size | |||
f.write("{},,\n".format(dist_info.joinpath("RECORD"))) | csv_writer.writerow((record, "", "")) | |||
def _get_file_hash(self, filepath): | def _get_file_hash(self, filepath: Path) -> str: | |||
hashsum = hashlib.sha256() | hashsum = hashlib.sha256() | |||
with filepath.open("rb") as src: | with filepath.open("rb") as src: | |||
while True: | while True: | |||
buf = src.read(1024 * 8) | buf = src.read(1024 * 8) | |||
if not buf: | if not buf: | |||
break | break | |||
hashsum.update(buf) | hashsum.update(buf) | |||
src.seek(0) | src.seek(0) | |||
return urlsafe_b64encode(hashsum.digest()).decode("ascii").rstrip("=") | return urlsafe_b64encode(hashsum.digest()).decode("ascii").rstrip("=") | |||
def _debug(self, msg): | def _debug(self, msg: str) -> None: | |||
if self._io.is_debug(): | if self._io.is_debug(): | |||
self._io.write_line(msg) | self._io.write_line(msg) | |||
End of changes. 37 change blocks. | ||||
77 lines changed or deleted | 106 lines changed or added |