Skip to content

Commit

Permalink
Hook for making local requirements publishable when creating a wheel …
Browse files Browse the repository at this point in the history
…or sdist
  • Loading branch information
idlsoft committed Jun 3, 2024
1 parent d73037f commit a13bf86
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 23 deletions.
3 changes: 2 additions & 1 deletion backend/src/hatchling/builders/hooks/plugin/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typing

from hatchling.builders.hooks.custom import CustomBuildHook
from hatchling.builders.hooks.publishable_locals import PublishableLocalsHook
from hatchling.builders.hooks.version import VersionBuildHook
from hatchling.plugin import hookimpl

Expand All @@ -12,4 +13,4 @@

@hookimpl
def hatch_register_build_hook() -> list[type[BuildHookInterface]]:
return [CustomBuildHook, VersionBuildHook] # type: ignore
return [CustomBuildHook, VersionBuildHook, PublishableLocalsHook] # type: ignore
10 changes: 10 additions & 0 deletions backend/src/hatchling/builders/hooks/plugin/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import TYPE_CHECKING, Any, Generic, cast

from packaging.requirements import Requirement

from hatchling.builders.config import BuilderConfigBound

if TYPE_CHECKING:
Expand Down Expand Up @@ -112,6 +114,14 @@ def target_name(self) -> str:
"""
return self.__target_name

def publishable_local(self, req: Requirement) -> Requirement:
"""
Converts a local requirement to publishable.
@param req: requirement
@return: publishable requirement or original
"""
return req

def dependencies(self) -> list[str]: # noqa: PLR6301
"""
A list of extra [dependencies](../../config/dependency.md) that must be installed
Expand Down
34 changes: 34 additions & 0 deletions backend/src/hatchling/builders/hooks/publishable_locals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
from typing import Optional

from packaging.requirements import Requirement

from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.metadata.core import ProjectMetadata


class PublishableLocalsHook(BuildHookInterface):
PLUGIN_NAME = 'publishable_locals'

def local_dependency_path(self, req: Requirement) -> Optional[str]:
if not req.url:
return None
req_url = req.url
if req_url.startswith("file://"):
req_url = req.url[len("file://"):]
elif ":" in req_url:
return None
if req_url.startswith("/"):
p_path = req_url
else:
p_path = os.path.normpath(os.path.join(self.root, req_url))
return p_path if os.path.isdir(p_path) else None

def publishable_local(self, req: Requirement) -> Requirement:
p_path = self.local_dependency_path(req)
if p_path:
p_meta = ProjectMetadata(p_path, plugin_manager=self.metadata.plugin_manager)
p_ver = p_meta.version
return Requirement(f"{req.name}=={p_ver}")
else:
return req
20 changes: 18 additions & 2 deletions backend/src/hatchling/builders/plugin/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
import os
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Callable, Generator, Generic, Iterable, cast
from typing import TYPE_CHECKING, Any, Callable, Generator, Generic, Iterable, cast, Optional

from packaging.requirements import Requirement

from hatchling.builders.config import BuilderConfig, BuilderConfigBound, env_var_enabled
from hatchling.builders.constants import EXCLUDED_DIRECTORIES, EXCLUDED_FILES, BuildEnvVars
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.builders.utils import get_relative_path, safe_walk
from hatchling.plugin.manager import PluginManagerBound

if TYPE_CHECKING:
from hatchling.bridge.app import Application
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.metadata.core import ProjectMetadata


Expand Down Expand Up @@ -142,6 +144,16 @@ def build(
# Allow inspection of configured build hooks and the order in which they run
build_data['build_hooks'] = tuple(configured_build_hooks)

pub_local_hook = None
for build_hook in build_hooks:
if type(build_hook).publishable_local is not BuildHookInterface.publishable_local:
if pub_local_hook is None:
pub_local_hook = build_hook
else:
raise ValueError(f'Hooks {pub_local_hook.PLUGIN_NAME} and {build_hook.PLUGIN_NAME} provide conflicting `publishable_local`')
if pub_local_hook:
build_data['publishable_local_function'] = pub_local_hook.publishable_local

# Execute all `initialize` build hooks
for build_hook in build_hooks:
build_hook.initialize(version, build_data)
Expand Down Expand Up @@ -439,3 +451,7 @@ def normalize_file_name_component(file_name: str) -> str:
https://peps.python.org/pep-0427/#escaping-and-unicode
"""
return re.sub(r'[^\w\d.]+', '_', file_name, flags=re.UNICODE)

@staticmethod
def publishable_local_function(build_data: dict[str, Any]) -> Optional[Callable[[Requirement], Requirement]]:
return build_data.get('publishable_local_function', None)
4 changes: 3 additions & 1 deletion backend/src/hatchling/builders/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ def build_standard(self, directory: str, **build_data: Any) -> str:
archive.addfile(tar_info)

archive.create_file(
self.config.core_metadata_constructor(self.metadata, extra_dependencies=build_data['dependencies']),
self.config.core_metadata_constructor(self.metadata,
extra_dependencies=build_data['dependencies'],
normalize_requirement=self.publishable_local_function(build_data)),
'PKG-INFO',
)

Expand Down
18 changes: 12 additions & 6 deletions backend/src/hatchling/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ def build_standard(self, directory: str, **build_data: Any) -> str:
record = archive.add_file(included_file)
records.write(record)

self.write_data(archive, records, build_data, build_data['dependencies'])
self.write_data(archive, records, build_data, build_data['dependencies'], enable_publishable_locals=True)

records.write((f'{archive.metadata_directory}/RECORD', '', ''))
archive.write_metadata('RECORD', records.construct())
Expand Down Expand Up @@ -602,13 +602,14 @@ def build_editable_explicit(self, directory: str, **build_data: Any) -> str:
return target

def write_data(
self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any], extra_dependencies: Sequence[str]
self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any], extra_dependencies: Sequence[str], enable_publishable_locals=False
) -> None:
self.add_shared_data(archive, records, build_data)
self.add_shared_scripts(archive, records, build_data)

# Ensure metadata is written last, see https://peps.python.org/pep-0427/#recommended-archiver-features
self.write_metadata(archive, records, build_data, extra_dependencies=extra_dependencies)
self.write_metadata(archive, records, build_data, extra_dependencies=extra_dependencies,
enable_publishable_locals=enable_publishable_locals)

def add_shared_data(self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None:
shared_data = dict(self.config.shared_data)
Expand Down Expand Up @@ -656,12 +657,14 @@ def write_metadata(
records: RecordFile,
build_data: dict[str, Any],
extra_dependencies: Sequence[str] = (),
enable_publishable_locals=False,
) -> None:
# <<< IMPORTANT >>>
# Ensure calls are ordered by the number of path components followed by the name of the components

# METADATA
self.write_project_metadata(archive, records, extra_dependencies=extra_dependencies)
self.write_project_metadata(archive, records, build_data, extra_dependencies=extra_dependencies,
enable_publishable_locals=enable_publishable_locals)

# WHEEL
self.write_archive_metadata(archive, records, build_data)
Expand Down Expand Up @@ -698,10 +701,13 @@ def write_entry_points_file(self, archive: WheelArchive, records: RecordFile) ->
records.write(record)

def write_project_metadata(
self, archive: WheelArchive, records: RecordFile, extra_dependencies: Sequence[str] = ()
self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any], extra_dependencies: Sequence[str] = (), enable_publishable_locals=False
) -> None:
normalize_requirement_f = self.publishable_local_function(build_data) if enable_publishable_locals else None
record = archive.write_metadata(
'METADATA', self.config.core_metadata_constructor(self.metadata, extra_dependencies=extra_dependencies)
'METADATA', self.config.core_metadata_constructor(self.metadata,
extra_dependencies=extra_dependencies,
normalize_requirement=normalize_requirement_f)
)
records.write(record)

Expand Down
48 changes: 35 additions & 13 deletions backend/src/hatchling/metadata/spec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any, Callable, Optional

from packaging.requirements import Requirement

if TYPE_CHECKING:
from hatchling.metadata.core import ProjectMetadata
Expand Down Expand Up @@ -196,7 +198,8 @@ def project_metadata_from_core_metadata(core_metadata: str) -> dict[str, Any]:
return metadata


def construct_metadata_file_1_2(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str:
def construct_metadata_file_1_2(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None,
normalize_requirement: Optional[Callable[[Requirement], Requirement]] = None) -> str:
"""
https://peps.python.org/pep-0345/
"""
Expand Down Expand Up @@ -244,8 +247,12 @@ def construct_metadata_file_1_2(metadata: ProjectMetadata, extra_dependencies: t
if metadata.core.requires_python:
metadata_file += f'Requires-Python: {metadata.core.requires_python}\n'

if metadata.core.dependencies:
for dependency in metadata.core.dependencies:
if metadata.core.dependencies_complex:
for dependency, req in metadata.core.dependencies_complex.items():
if normalize_requirement:
new_req = normalize_requirement(req)
if new_req is not req:
dependency = str(new_req)
metadata_file += f'Requires-Dist: {dependency}\n'

if extra_dependencies:
Expand All @@ -255,7 +262,8 @@ def construct_metadata_file_1_2(metadata: ProjectMetadata, extra_dependencies: t
return metadata_file


def construct_metadata_file_2_1(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str:
def construct_metadata_file_2_1(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None,
normalize_requirement: Optional[Callable[[Requirement], Requirement]] = None) -> str:
"""
https://peps.python.org/pep-0566/
"""
Expand Down Expand Up @@ -310,8 +318,12 @@ def construct_metadata_file_2_1(metadata: ProjectMetadata, extra_dependencies: t
if metadata.core.requires_python:
metadata_file += f'Requires-Python: {metadata.core.requires_python}\n'

if metadata.core.dependencies:
for dependency in metadata.core.dependencies:
if metadata.core.dependencies_complex:
for dependency, req in metadata.core.dependencies_complex.items():
if normalize_requirement:
new_req = normalize_requirement(req)
if new_req is not req:
dependency = str(new_req)
metadata_file += f'Requires-Dist: {dependency}\n'

if extra_dependencies:
Expand All @@ -337,7 +349,8 @@ def construct_metadata_file_2_1(metadata: ProjectMetadata, extra_dependencies: t
return metadata_file


def construct_metadata_file_2_2(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str:
def construct_metadata_file_2_2(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None,
normalize_requirement: Optional[Callable[[Requirement], Requirement]] = None) -> str:
"""
https://peps.python.org/pep-0643/
"""
Expand Down Expand Up @@ -401,8 +414,12 @@ def construct_metadata_file_2_2(metadata: ProjectMetadata, extra_dependencies: t
if metadata.core.requires_python:
metadata_file += f'Requires-Python: {metadata.core.requires_python}\n'

if metadata.core.dependencies:
for dependency in metadata.core.dependencies:
if metadata.core.dependencies_complex:
for dependency, req in metadata.core.dependencies_complex.items():
if normalize_requirement:
new_req = normalize_requirement(req)
if new_req is not req:
dependency = str(new_req)
metadata_file += f'Requires-Dist: {dependency}\n'

if extra_dependencies:
Expand All @@ -428,7 +445,8 @@ def construct_metadata_file_2_2(metadata: ProjectMetadata, extra_dependencies: t
return metadata_file


def construct_metadata_file_2_3(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str:
def construct_metadata_file_2_3(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None,
normalize_requirement: Optional[Callable[[Requirement], Requirement]] = None) -> str:
"""
https://peps.python.org/pep-0639/
"""
Expand Down Expand Up @@ -492,8 +510,12 @@ def construct_metadata_file_2_3(metadata: ProjectMetadata, extra_dependencies: t
if metadata.core.requires_python:
metadata_file += f'Requires-Python: {metadata.core.requires_python}\n'

if metadata.core.dependencies:
for dependency in metadata.core.dependencies:
if metadata.core.dependencies_complex:
for dependency, req in metadata.core.dependencies_complex.items():
if normalize_requirement:
new_req = normalize_requirement(req)
if new_req is not req:
dependency = str(new_req)
metadata_file += f'Requires-Dist: {dependency}\n'

if extra_dependencies:
Expand Down

0 comments on commit a13bf86

Please sign in to comment.