Skip to content

Commit

Permalink
[DRAFT] Add version to metadata hook, use version to retrieve deps
Browse files Browse the repository at this point in the history
This is a very draft proposal on how to fix pypa#1348 and pypa#1349 - more
to see if this is a good direction. It should likely be split to
two PRs (and of course tests and docs are needed):

* extending custom metadata plugin to allow passing version through
  hook's version property to distinguish standard and editable builds
  (also including extending the hatchling CLIs.

* adding version to CLI where hatch queries hatchling to include standard/
  editable version when querying for available dependencies.

Not all changes have been yet applied, this is more to check if
this is the right direction.
  • Loading branch information
potiuk committed May 29, 2024
1 parent d73037f commit c02df17
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ __pycache__/
/.ruff_cache/
/.vscode/
/backend/dist/
/backend/.idea/
/dist/
/site/

Expand Down
2 changes: 1 addition & 1 deletion backend/src/hatchling/builders/plugin/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def build(

for version in versions:
self.app.display_debug(f'Building `{self.PLUGIN_NAME}` version `{version}`')
self.metadata.run_metadata_hooks(build_version=version)

build_data = self.get_default_build_data()
self.set_build_data_defaults(build_data)
Expand Down Expand Up @@ -291,7 +292,6 @@ def metadata(self) -> ProjectMetadata:
from hatchling.metadata.core import ProjectMetadata

self.__metadata = ProjectMetadata(self.root, self.plugin_manager, self.__raw_config)

return self.__metadata

@property
Expand Down
1 change: 0 additions & 1 deletion backend/src/hatchling/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,6 @@ def build_editable_detection(self, directory: str, **build_data: Any) -> str:
from editables import EditableProject

build_data['tag'] = self.get_default_tag()

with WheelArchive(
self.artifact_project_id, reproducible=self.config.reproducible
) as archive, RecordFile() as records:
Expand Down
4 changes: 3 additions & 1 deletion backend/src/hatchling/cli/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def metadata_impl(
called_by_app: bool, # noqa: ARG001
field: str,
compact: bool,
version: str = "standard",
) -> None:
import json
import os
Expand All @@ -22,7 +23,7 @@ def metadata_impl(

root = os.getcwd()
plugin_manager = PluginManager()
project_metadata = ProjectMetadata(root, plugin_manager)
project_metadata = ProjectMetadata(root, plugin_manager, build_versions=[version])

metadata = resolve_metadata_fields(project_metadata)
if field: # no cov
Expand Down Expand Up @@ -55,4 +56,5 @@ def metadata_command(
parser.add_argument('field', nargs='?')
parser.add_argument('-c', '--compact', action='store_true')
parser.add_argument('--app', dest='called_by_app', action='store_true', help=argparse.SUPPRESS)
parser.add_argument('--version', dest='version', default="standard")
parser.set_defaults(func=metadata_impl)
60 changes: 35 additions & 25 deletions backend/src/hatchling/metadata/core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import contextlib
import os
import sys
from contextlib import suppress
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Generic, cast
from typing import TYPE_CHECKING, Any, Generic, cast, Generator

from hatchling.metadata.utils import (
format_dependency,
Expand Down Expand Up @@ -183,33 +184,36 @@ def core(self) -> CoreMetadata:

# Save the fields
_ = self.dynamic
self._core = metadata
self.run_metadata_hooks()
return self._core

metadata_hooks = self.hatch.metadata.hooks
if metadata_hooks:
static_fields = set(self.core_raw_metadata)
if 'version' in self.hatch.config:
self._version = self._get_version(metadata)
self.core_raw_metadata['version'] = self.version

if metadata.dynamic:
for metadata_hook in metadata_hooks.values():
def run_metadata_hooks(self, build_version: str = None) -> None:
metadata = self._core
metadata_hooks = self.hatch.metadata.hooks
if metadata_hooks:
static_fields = set(self.core_raw_metadata)
if 'version' in self.hatch.config:
self._version = self._get_version(metadata)
self.core_raw_metadata['version'] = self.version

if metadata.dynamic or metadata.original_dynamic:
if not metadata.original_dynamic:
metadata._original_dynamic = deepcopy(metadata.dynamic)
for metadata_hook in metadata_hooks.values():
with metadata_hook.set_build_version(build_version):
metadata_hook.update(self.core_raw_metadata)
metadata.add_known_classifiers(metadata_hook.get_known_classifiers())

new_fields = set(self.core_raw_metadata) - static_fields
for new_field in new_fields:
if new_field in metadata.dynamic:
metadata.dynamic.remove(new_field)
else:
message = (
f'The field `{new_field}` was set dynamically and therefore must be '
f'listed in `project.dynamic`'
)
raise ValueError(message)

self._core = metadata

return self._core
new_fields = set(self.core_raw_metadata) - static_fields
for new_field in new_fields:
if new_field in metadata.dynamic:
metadata.dynamic.remove(new_field)
if new_field not in metadata.dynamic and new_field not in metadata.original_dynamic:
message = (
f'The field `{new_field}` was set dynamically and therefore must be '
f'listed in `project.dynamic`'
)
raise ValueError(message)

@property
def hatch(self) -> HatchMetadata:
Expand Down Expand Up @@ -389,10 +393,15 @@ def __init__(
self._optional_dependencies_complex: dict[str, dict[str, Requirement]] | None = None
self._optional_dependencies: dict[str, list[str]] | None = None
self._dynamic: list[str] | None = None
self._original_dynamic: list[str] | None = None

# Indicates that the version has been successfully set dynamically
self._version_set: bool = False

@property
def original_dynamic(self) -> list[str]:
return self._original_dynamic or []

@property
def raw_name(self) -> str:
"""
Expand Down Expand Up @@ -1367,6 +1376,7 @@ def dynamic(self) -> list[str]:

return self._dynamic


def add_known_classifiers(self, classifiers: list[str]) -> None:
self._extra_classifiers.update(classifiers)

Expand Down
17 changes: 17 additions & 0 deletions backend/src/hatchling/metadata/plugin/interface.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import contextlib
from abc import ABC, abstractmethod
from typing import Generator


class MetadataHookInterface(ABC): # no cov
Expand Down Expand Up @@ -34,6 +36,7 @@ def hatch_register_metadata_hook():
def __init__(self, root: str, config: dict) -> None:
self.__root = root
self.__config = config
self.__build_version: str | None = None

@property
def root(self) -> str:
Expand All @@ -53,6 +56,13 @@ def config(self) -> dict:
"""
return self.__config

@property
def build_version(self) -> str | None:
"""
Gets the version of build (standard/editable) that is being run.
"""
return self.__build_version

@abstractmethod
def update(self, metadata: dict) -> None:
"""
Expand All @@ -64,3 +74,10 @@ def get_known_classifiers(self) -> list[str]: # noqa: PLR6301
This returns extra classifiers that should be considered valid in addition to the ones known to PyPI.
"""
return []

@contextlib.contextmanager
def set_build_version(self, build_version: str) -> Generator[None, None, None]:
old_build_version = self.build_version
self.__build_version = build_version
yield
self.__build_version = old_build_version
3 changes: 2 additions & 1 deletion src/hatch/utils/dep.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ def get_project_dependencies_complex(
from packaging.requirements import Requirement

with environment.root.as_cwd(), environment.build_environment(environment.metadata.build.requires):
command = ['python', '-u', '-W', 'ignore', '-m', 'hatchling', 'metadata', '--compact']
command = ['python', '-u', '-W', 'ignore', '-m', 'hatchling', 'metadata', '--compact',
"--version", "editable" if environment.dev_mode else "standard"]
output = environment.platform.check_command_output(
command,
# Only capture stdout
Expand Down

0 comments on commit c02df17

Please sign in to comment.