-
Notifications
You must be signed in to change notification settings - Fork 76
feat: add template dynamic-metadata plugin and support requesting other fields #1047
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
Merged
+351
−107
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
56176fd
feat: add template dynamic-metadata plugin and dynamic_metadata_needs
henryiii 2952e9f
style: pre-commit fixes
pre-commit-ci[bot] c2c2dd2
fix: allow more fields in regex
henryiii cbddac0
fix: ensure needs always works
henryiii c15955c
refactor: implicit needs
henryiii 69e8424
fix: avoid triggering with containership checks
henryiii d5e70dc
tests: add tests for regex and template
henryiii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import dataclasses | ||
| import importlib | ||
| import inspect | ||
| import sys | ||
| from collections.abc import Iterator, Mapping | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING, Any, Protocol, Union, runtime_checkable | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Generator, Iterable | ||
|
|
||
| StrMapping = Mapping[str, Any] | ||
| else: | ||
| StrMapping = Mapping | ||
|
|
||
| from ..metadata import _ALL_FIELDS | ||
|
|
||
| __all__ = ["load_provider", "process_dynamic_metadata"] | ||
|
|
||
|
|
||
| def __dir__() -> list[str]: | ||
| return __all__ | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class DynamicMetadataProtocol(Protocol): | ||
| def dynamic_metadata( | ||
| self, | ||
| fields: Iterable[str], | ||
| settings: dict[str, Any], | ||
| project: Mapping[str, Any], | ||
| ) -> dict[str, Any]: ... | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class DynamicMetadataRequirementsProtocol(DynamicMetadataProtocol, Protocol): | ||
| def get_requires_for_dynamic_metadata( | ||
| self, settings: dict[str, Any] | ||
| ) -> list[str]: ... | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class DynamicMetadataWheelProtocol(DynamicMetadataProtocol, Protocol): | ||
| def dynamic_wheel(self, field: str, settings: Mapping[str, Any]) -> bool: ... | ||
|
|
||
|
|
||
| DMProtocols = Union[ | ||
| DynamicMetadataProtocol, | ||
| DynamicMetadataRequirementsProtocol, | ||
| DynamicMetadataWheelProtocol, | ||
| ] | ||
|
|
||
|
|
||
| def load_provider( | ||
| provider: str, | ||
| provider_path: str | None = None, | ||
| ) -> DMProtocols: | ||
| if provider_path is None: | ||
| return importlib.import_module(provider) | ||
|
|
||
| if not Path(provider_path).is_dir(): | ||
| msg = "provider-path must be an existing directory" | ||
| raise AssertionError(msg) | ||
|
|
||
| try: | ||
| sys.path.insert(0, provider_path) | ||
| return importlib.import_module(provider) | ||
| finally: | ||
| sys.path.pop(0) | ||
|
|
||
|
|
||
| def _load_dynamic_metadata( | ||
| metadata: Mapping[str, Mapping[str, str]], | ||
| ) -> Generator[tuple[str, DMProtocols, dict[str, Any]], None, None]: | ||
| for field, orig_config in metadata.items(): | ||
| if "provider" not in orig_config: | ||
| msg = "Missing provider in dynamic metadata" | ||
| raise KeyError(msg) | ||
|
|
||
| if field not in _ALL_FIELDS: | ||
| msg = f"{field} is not a valid field" | ||
| raise KeyError(msg) | ||
| config = dict(orig_config) | ||
| provider = config.pop("provider") | ||
| provider_path = config.pop("provider-path", None) | ||
| loaded_provider = load_provider(provider, provider_path) | ||
| yield field, loaded_provider, config | ||
|
|
||
|
|
||
| @dataclasses.dataclass | ||
| class DynamicPyProject(StrMapping): | ||
| settings: dict[str, dict[str, Any]] | ||
| project: dict[str, Any] | ||
| providers: dict[str, DMProtocols] | ||
|
|
||
| def __getitem__(self, key: str) -> Any: | ||
| # Try to get the settings from either the static file or dynamic metadata provider | ||
| if key in self.project: | ||
| return self.project[key] | ||
|
|
||
| # Check if we are in a loop, i.e. something else is already requesting | ||
| # this key while trying to get another key | ||
| if key not in self.providers: | ||
| dep_type = "missing" if key in self.settings else "circular" | ||
| msg = f"Encountered a {dep_type} dependency at {key}" | ||
| raise ValueError(msg) | ||
|
|
||
| provider = self.providers.pop(key) | ||
| sig = inspect.signature(provider.dynamic_metadata) | ||
| if len(sig.parameters) < 3: | ||
| # Backcompat for dynamic_metadata without metadata dict | ||
| self.project[key] = provider.dynamic_metadata( # type: ignore[call-arg] | ||
| key, self.settings[key] | ||
| ) | ||
| else: | ||
| self.project[key] = provider.dynamic_metadata(key, self.settings[key], self) | ||
| self.project["dynamic"].remove(key) | ||
|
|
||
| return self.project[key] | ||
|
|
||
| def __iter__(self) -> Iterator[str]: | ||
| # Iterate over the keys of the static settings | ||
| yield from self.project | ||
|
|
||
| # Iterate over the keys of the dynamic metadata providers | ||
| yield from self.providers | ||
|
|
||
| def __len__(self) -> int: | ||
| return len(self.project) + len(self.providers) | ||
|
|
||
| def __contains__(self, key: object) -> bool: | ||
| return key in self.project or key in self.providers | ||
|
|
||
|
|
||
| def process_dynamic_metadata( | ||
| project: Mapping[str, Any], | ||
| metadata: Mapping[str, Mapping[str, Any]], | ||
| ) -> dict[str, Any]: | ||
| initial = {f: (p, c) for (f, p, c) in _load_dynamic_metadata(metadata)} | ||
|
|
||
| settings = DynamicPyProject( | ||
| settings={f: c for f, (_, c) in initial.items()}, | ||
| project=dict(project), | ||
| providers={k: v for k, (v, _) in initial.items()}, | ||
| ) | ||
|
|
||
| return dict(settings) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,87 @@ | ||
| from __future__ import annotations | ||
|
|
||
| __all__: list[str] = [] | ||
| import typing | ||
|
|
||
| if typing.TYPE_CHECKING: | ||
| from collections.abc import Callable | ||
|
|
||
|
|
||
| __all__: list[str] = ["_ALL_FIELDS", "_process_dynamic_metadata"] | ||
|
|
||
|
|
||
| # Name is not dynamically settable, so not in this list | ||
| _STR_FIELDS = frozenset( | ||
| [ | ||
| "version", | ||
| "description", | ||
| "requires-python", | ||
| "license", | ||
| ] | ||
| ) | ||
|
|
||
| # Dynamic is not dynamically settable, so not in this list | ||
| _LIST_STR_FIELDS = frozenset( | ||
| [ | ||
| "classifiers", | ||
| "keywords", | ||
| "dependencies", | ||
| "license_files", | ||
| ] | ||
| ) | ||
|
|
||
| _DICT_STR_FIELDS = frozenset( | ||
| [ | ||
| "urls", | ||
| "authors", | ||
| "maintainers", | ||
| ] | ||
| ) | ||
|
|
||
| # "dynamic" and "name" can't be set or requested | ||
| _ALL_FIELDS = ( | ||
| _STR_FIELDS | ||
| | _LIST_STR_FIELDS | ||
| | _DICT_STR_FIELDS | ||
| | frozenset( | ||
| [ | ||
| "optional-dependencies", | ||
| "readme", | ||
| ] | ||
| ) | ||
| ) | ||
|
|
||
| T = typing.TypeVar("T", bound="str | list[str] | dict[str, str] | dict[str, list[str]]") | ||
|
|
||
|
|
||
| def _process_dynamic_metadata(field: str, action: Callable[[str], str], result: T) -> T: | ||
| """ | ||
| Helper function for processing the an action on the various possible metadata fields. | ||
| """ | ||
|
|
||
| if field in _STR_FIELDS: | ||
| if not isinstance(result, str): | ||
| msg = f"Field {field!r} must be a string" | ||
| raise RuntimeError(msg) | ||
| return action(result) # type: ignore[return-value] | ||
| if field in _LIST_STR_FIELDS: | ||
| if not (isinstance(result, list) and all(isinstance(r, str) for r in result)): | ||
| msg = f"Field {field!r} must be a list of strings" | ||
| raise RuntimeError(msg) | ||
| return [action(r) for r in result] # type: ignore[return-value] | ||
| if field in _DICT_STR_FIELDS | {"readme"}: | ||
| if not isinstance(result, dict) or not all( | ||
| isinstance(v, str) for v in result.values() | ||
| ): | ||
| msg = f"Field {field!r} must be a dictionary of strings" | ||
| raise RuntimeError(msg) | ||
| return {k: action(v) for k, v in result.items()} # type: ignore[return-value] | ||
| if field == "optional-dependencies": | ||
| if not isinstance(result, dict) or not all( | ||
| isinstance(v, list) for v in result.values() | ||
| ): | ||
| msg = "Field 'optional-dependencies' must be a dictionary of lists" | ||
| raise RuntimeError(msg) | ||
| return {k: [action(r) for r in v] for k, v in result.items()} # type: ignore[return-value] | ||
|
|
||
| msg = f"Unsupported field {field!r} for action" | ||
| raise RuntimeError(msg) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.