Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support for relative include of non-package modules ("workspaces") #273

Closed
wants to merge 41 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6abcf42
feat: add a workspace property to the Poetry class, set to the worksp…
DavidVujic Jan 28, 2022
65a706e
feat: use the workspace (when not None) to determine if a module shou…
DavidVujic Jan 29, 2022
b84ce16
fix: create a namespaced path to a shared package include existing in…
DavidVujic Feb 5, 2022
3911598
fix: add missing pathlib.Path requirement
DavidVujic Mar 26, 2022
e4857ab
fix: add missing pathlib.Path requirement
DavidVujic Mar 26, 2022
56f7958
fix: use modern syntax for union data types, according to the current…
DavidVujic Apr 3, 2022
33f01bb
introduce default_target_dir on builders
dimbleby May 7, 2022
227e2d5
fix: check if is drive root in ways that work cross OS platform
DavidVujic Aug 31, 2022
802dcaa
fix: calculate path differently if the project is in a workspace or n…
DavidVujic Sep 11, 2022
d1b1bae
fix: add missing pathlib.Path requirement
DavidVujic Mar 26, 2022
2b89a79
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 26, 2022
ff84e64
fix: add missing pathlib.Path requirement
DavidVujic Mar 26, 2022
f484a63
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 16, 2022
1a4a392
fix: make sure that the sdist setup.py is generated correctly, when a…
DavidVujic Oct 16, 2022
ceefbbf
fix: add custom implementation of the pathlib is_relative_to, to supp…
DavidVujic Oct 16, 2022
25ea22c
docs: add docstring to the poetry.core.util.namespacing module
DavidVujic Oct 19, 2022
3362e11
test: add unit tests for poetry.core.utils.namespacing
DavidVujic Oct 19, 2022
8d8a10e
docs: add docstring for the poetry.core.utils workspace module
DavidVujic Oct 19, 2022
3ca4093
refactor: explicit check for workspace is None in BuilderIncludeFile …
DavidVujic Oct 19, 2022
a08a052
refactor: use Builder boolean function to determine if in workspace o…
DavidVujic Oct 19, 2022
ca021d0
test: add unit tests for poetry.core.masonry.builder BuildIncludeFile
DavidVujic Oct 19, 2022
7643788
test: add unit tests for poetry.core.masonry.builder BuildIncludeFile
DavidVujic Oct 19, 2022
84eacdd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 19, 2022
0bbac01
feat: explicit check if a workspace is a Poetry workspace when parsin…
DavidVujic Oct 22, 2022
f12935e
fix: Move third-party import 'tomlkit' into a type-checking block
DavidVujic Oct 22, 2022
7708fdd
fix: sdist package_dir and package properties, wheel source paths whe…
DavidVujic Oct 22, 2022
caaa7ca
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 22, 2022
5a76681
refactor: move the workspace adjusted response from sdist.find_packag…
DavidVujic Oct 22, 2022
877b2dc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 22, 2022
4cf2525
fix: start looking for the workspace root from where the actual pypro…
DavidVujic Oct 22, 2022
deb1016
Move built-in import 'pathlib.Path' into a type-checking block
DavidVujic Oct 22, 2022
c2cb9cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 22, 2022
ed3f228
WIP: generate a sdist compliant pyproject.toml when running 'poetry b…
DavidVujic Oct 23, 2022
985ace6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 23, 2022
4d56fb0
WIP: generate a sdist compliant pyproject.toml when running 'poetry b…
DavidVujic Oct 23, 2022
d983006
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 23, 2022
9aa69f3
WIP: generate a sdist compliant pyproject.toml when running 'poetry b…
DavidVujic Oct 23, 2022
1222f80
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 23, 2022
bcf3939
refactor: extract the find packages builder feature specific for work…
DavidVujic Oct 24, 2022
e6674e7
refactor: extract the tar info creation into a function
DavidVujic Oct 24, 2022
b9d806f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 24, 2022
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
56 changes: 48 additions & 8 deletions src/poetry/core/masonry/builders/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from pathlib import Path
from typing import TYPE_CHECKING

from poetry.core.utils import helpers
from poetry.core.utils import namespacing


if TYPE_CHECKING:
from poetry.core.poetry import Poetry
Expand Down Expand Up @@ -43,6 +46,7 @@ def __init__(
self._path: Path = poetry.file.parent
self._excluded_files: set[str] | None = None
self._executable = Path(executable or sys.executable)
self._workspace = poetry.workspace

packages = []
for p in self._package.packages:
Expand Down Expand Up @@ -183,21 +187,35 @@ def find_files_to_add(self, exclude_build: bool = True) -> set[BuildIncludeFile]
path=current_file,
project_root=self._path,
source_root=source_root,
workspace=self._workspace,
)

if not current_file.is_dir() and not self.is_excluded(
include_file.relative_to_source_root()
):
to_add.add(include_file)
if not current_file.is_dir():
include_file_path = (
include_file.relative_to_workspace()
if self.is_in_workspace()
else include_file.relative_to_source_root()
)
if not self.is_excluded(include_file_path):
to_add.add(include_file)
continue

include_file = BuildIncludeFile(
path=file, project_root=self._path, source_root=source_root
path=file,
project_root=self._path,
source_root=source_root,
workspace=self._workspace,
)

include_file_path = (
include_file.relative_to_workspace()
if self.is_in_workspace()
else include_file.relative_to_project_root()
)

if self.is_excluded(
include_file.relative_to_project_root()
) and isinstance(include, PackageInclude):
if self.is_excluded(include_file_path) and isinstance(
include, PackageInclude
):
continue

if file.suffix == ".pyc":
Expand All @@ -213,6 +231,7 @@ def find_files_to_add(self, exclude_build: bool = True) -> set[BuildIncludeFile]
path=self._package.build_script,
project_root=self._path,
source_root=self._path,
workspace=self._workspace,
)
)

Expand Down Expand Up @@ -353,22 +372,28 @@ def convert_author(cls, author: str) -> dict[str, str]:

return {"name": name, "email": email}

def is_in_workspace(self) -> bool:
return self._workspace is not None


class BuildIncludeFile:
def __init__(
self,
path: Path | str,
project_root: Path | str,
source_root: Path | str | None = None,
workspace: Path | None = None,
) -> None:
"""
:param project_root: the full path of the project's root
:param path: a full path to the file to be included
:param source_root: the root path to resolve to
:param workspace: the full path of the workspace root
"""
self.path = Path(path)
self.project_root = Path(project_root).resolve()
self.source_root = None if not source_root else Path(source_root).resolve()
self.workspace = workspace
if not self.path.is_absolute() and self.source_root:
self.path = self.source_root / self.path
else:
Expand Down Expand Up @@ -396,3 +421,18 @@ def relative_to_source_root(self) -> Path:
return self.path.relative_to(self.source_root)

return self.path

def relative_to_workspace(self) -> Path:
if self.workspace is not None:
return self.path.relative_to(self.workspace)

return self.path

def calculated_path(self) -> Path:
if self.workspace is not None:
if helpers.is_path_relative_to_other(self.path, self.project_root):
return self.relative_to_source_root()

return namespacing.create_namespaced_path(self.path, self.workspace)

return self.relative_to_source_root()
58 changes: 44 additions & 14 deletions src/poetry/core/masonry/builders/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

from poetry.core.masonry.builders.builder import Builder
from poetry.core.masonry.builders.builder import BuildIncludeFile
from poetry.core.masonry.utils.dist_toml import create_valid_dist_project_file
from poetry.core.masonry.utils.helpers import distribution_name
from poetry.core.utils import namespacing


if TYPE_CHECKING:
Expand Down Expand Up @@ -78,10 +80,10 @@ def build(

files_to_add = self.find_files_to_add(exclude_build=False)

for file in sorted(files_to_add, key=lambda x: x.relative_to_source_root()):
for file in sorted(files_to_add, key=lambda x: x.calculated_path()):
tar_info = tar.gettarinfo(
str(file.path),
arcname=pjoin(tar_dir, str(file.relative_to_source_root())),
arcname=pjoin(tar_dir, str(file.calculated_path())),
)
tar_info = self.clean_tarinfo(tar_info)

Expand All @@ -91,20 +93,23 @@ def build(
else:
tar.addfile(tar_info) # Symlinks & ?

if self.is_in_workspace():
dist_pyproject = create_valid_dist_project_file(
self._poetry.pyproject.data
)
tar_info = self.create_tarinfo(
tar_dir, "pyproject.toml", dist_pyproject
)
tar.addfile(tar_info, BytesIO(dist_pyproject))

if self._poetry.package.build_should_generate_setup():
setup = self.build_setup()
tar_info = tarfile.TarInfo(pjoin(tar_dir, "setup.py"))
tar_info.size = len(setup)
tar_info.mtime = 0
tar_info = self.clean_tarinfo(tar_info)
tar_info = self.create_tarinfo(tar_dir, "setup.py", setup)
tar.addfile(tar_info, BytesIO(setup))

pkg_info = self.build_pkg_info()

tar_info = tarfile.TarInfo(pjoin(tar_dir, "PKG-INFO"))
tar_info.size = len(pkg_info)
tar_info.mtime = 0
tar_info = self.clean_tarinfo(tar_info)
tar_info = self.create_tarinfo(tar_dir, "PKG-INFO", pkg_info)
tar.addfile(tar_info, BytesIO(pkg_info))
finally:
tar.close()
Expand All @@ -113,6 +118,12 @@ def build(
logger.info(f"Built <comment>{target.name}</comment>")
return target

def create_tarinfo(self, tar_dir: str, name: str, data: bytes) -> tarfile.TarInfo:
tar_info = tarfile.TarInfo(pjoin(tar_dir, name))
tar_info.size = len(data)
tar_info.mtime = 0
return self.clean_tarinfo(tar_info)

def build_setup(self) -> bytes:
from poetry.core.masonry.utils.package_include import PackageInclude

Expand All @@ -135,7 +146,11 @@ def build_setup(self) -> bytes:

if isinstance(include, PackageInclude):
if include.is_package():
pkg_dir, _packages, _package_data = self.find_packages(include)
pkg_dir, _packages, _package_data = (
self.find_packages_for_workspace(include)
if self.is_in_workspace()
else self.find_packages(include)
)

if pkg_dir is not None:
pkg_root = os.path.relpath(pkg_dir, str(self._path))
Expand Down Expand Up @@ -318,6 +333,17 @@ def find_nearest_pkg(rel_path: str) -> tuple[str, str]:

return pkgdir, sorted(packages), pkg_data

def find_packages_for_workspace(
self, include: PackageInclude
) -> tuple[str | None, list[str], dict[str, list[str]]]:
_, packages, package_data = self.find_packages(include)

pkgdir = include.get_package_dir()
ns_path = include.get_namespace_path()
packages = packages if pkgdir else [namespacing.convert_to_namespace(ns_path)]

return pkgdir, sorted(packages), package_data

def find_files_to_add(self, exclude_build: bool = False) -> set[BuildIncludeFile]:
to_add = super().find_files_to_add(exclude_build)

Expand All @@ -328,18 +354,22 @@ def find_files_to_add(self, exclude_build: bool = False) -> set[BuildIncludeFile
additional_files.update(self.convert_script_files())

# Include project files
additional_files.add(Path("pyproject.toml"))
if not self.is_in_workspace():
additional_files.add(Path("pyproject.toml"))

# add readme if it is specified
if "readme" in self._poetry.local_config:
additional_files.add(self._poetry.local_config["readme"])

for additional_file in additional_files:
file = BuildIncludeFile(
path=additional_file, project_root=self._path, source_root=self._path
path=additional_file,
project_root=self._path,
source_root=self._path,
workspace=self._workspace,
)
if file.path.exists():
logger.debug(f"Adding: {file.relative_to_source_root()}")
logger.debug(f"Adding: {file.calculated_path()}")
to_add.add(file)

return to_add
Expand Down
2 changes: 1 addition & 1 deletion src/poetry/core/masonry/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def _copy_module(self, wheel: zipfile.ZipFile) -> None:
# Walk the files and compress them,
# sorting everything so the order is stable.
for file in sorted(to_add, key=lambda x: x.path):
self._add_file(wheel, file.path, file.relative_to_source_root())
self._add_file(wheel, file.path, file.calculated_path())

def _write_metadata(self, wheel: zipfile.ZipFile) -> None:
if (
Expand Down
63 changes: 63 additions & 0 deletions src/poetry/core/masonry/utils/dist_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import tomlkit


def to_valid_dist_package(package: dict[str, str]) -> dict[str, str]:
"""Returns a [tool.poetry] packages list item.

Rearranges the "include" and "from" attributes for relative included packages,
adapting to the sdist build output.

The output from this function will reflect the build output to:
{"include": "foo/bar"}
"""
if ".." not in package.get("from", ""):
return package

return {"include": package["include"]}


def to_valid_dist_packages(data: tomlkit.toml.TOMLDocument) -> list[dict[str, str]]:
"""Returns a [tool.poetry] packages section.

Rearrange packages with relative paths, to reflect the sdist build output.

Example: a pyproject.toml with relative packages.
packages = [{"include": "foo/bar", from: "../../components"}]

When building an sdist (using the "poetry build" command),
the packages will be collected and copied into a local directory.
/dist
/setup.py
/foo/bar

The end result in a toml file from running this function would be:
packages = [{"include" = "foo/bar"}]
"""
packages = data["tool"]["poetry"]["packages"]

return [to_valid_dist_package(p) for p in packages]


def create_valid_dist_project_file(data: tomlkit.toml.TOMLDocument) -> bytes:
"""Create a project file

Returns a project file with any relative package includes rearranged,
according to the expected sdist build output.
"""
original = tomlkit.dumps(data)
copy = tomlkit.parse(original)

dist_packages = to_valid_dist_packages(copy)

copy["tool"]["poetry"]["packages"].clear()

for package in dist_packages:
copy["tool"]["poetry"]["packages"].append(package)

copy["tool"]["poetry"]["packages"].multiline(True)

content = tomlkit.dumps(copy) or ""

return content.encode()
4 changes: 4 additions & 0 deletions src/poetry/core/masonry/utils/include.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ def __init__(
def base(self) -> Path:
return self._base

@property
def include(self) -> str:
return self._include

@property
def elements(self) -> list[Path]:
return self._elements
Expand Down
9 changes: 9 additions & 0 deletions src/poetry/core/masonry/utils/package_include.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,12 @@ def check_elements(self) -> PackageInclude:
self._is_module = True

return self

def get_namespace_path(self) -> str:
return self.include

def get_package_dir(self) -> str | None:
if self.source and ".." not in self.source:
return self.source

return None
6 changes: 6 additions & 0 deletions src/poetry/core/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ def __init__(
package: ProjectPackage,
) -> None:
from poetry.core.pyproject.toml import PyProjectTOML
from poetry.core.utils import workspaces

self._pyproject = PyProjectTOML(file)
self._package = package
self._local_config = local_config
self._workspace = workspaces.find_workspace_root(file.parent)

@property
def pyproject(self) -> PyProjectTOML:
Expand All @@ -41,5 +43,9 @@ def package(self) -> ProjectPackage:
def local_config(self) -> dict[str, Any]:
return self._local_config

@property
def workspace(self) -> Path | None:
return self._workspace

def get_project_config(self, config: str, default: Any = None) -> Any:
return self._local_config.get("config", {}).get(config, default)
16 changes: 16 additions & 0 deletions src/poetry/core/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,19 @@ def readme_content_type(path: str | Path) -> str:
return "text/markdown"
else:
return "text/plain"


def is_path_relative_to_other(path: Path, other: Path) -> bool:
"""Return True if the path is relative to another path or False.

Note:
this is a custom implementation of Path.is_relative_to
that was introduced to pathlib in Python 3.9.

This is needed because Poetry support Python versions prior to that.
"""
try:
path.relative_to(other)
return True
except ValueError:
return False
Loading