-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Implement PEP 643 (Dynamic
field for core metadata)
#4698
Merged
+471
−59
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
aad7d3d
Prepare test for PEP 643 by removing checks on Metadata-Version and D…
abravalheri 22b3bf3
First draft implementation of '_static' in preparation for PEP 643
abravalheri 5124e8c
Modify Metadata-Version expectation in test_egg_info
abravalheri fad8675
Use _static.{List,Dict} and an attribute to track modifications inste…
abravalheri 0adaa83
Add tests for Dynamic core metadata for setup.cfg
abravalheri cf576e2
Add tests for Dynamic core metadata for pyproject.toml
abravalheri 8b4c8a3
Add tests for static 'attr' directive
abravalheri f699fd8
Fix spelling error
abravalheri 8b22d73
Mark values from pyproject.toml as static
abravalheri 770b4fc
Remove test workaround for unmarked static values from pyproject.toml
abravalheri b055895
Add extra tests for static/dynamic metadata
abravalheri a50f6e2
Fix _static.Dict.__ior__ for Python 3.8
abravalheri 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 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 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,188 @@ | ||
from functools import wraps | ||
from typing import Any, TypeVar | ||
|
||
import packaging.specifiers | ||
|
||
from .warnings import SetuptoolsDeprecationWarning | ||
|
||
|
||
class Static: | ||
""" | ||
Wrapper for built-in object types that are allow setuptools to identify | ||
static core metadata (in opposition to ``Dynamic``, as defined :pep:`643`). | ||
The trick is to mark values with :class:`Static` when they come from | ||
``pyproject.toml`` or ``setup.cfg``, so if any plugin overwrite the value | ||
with a built-in, setuptools will be able to recognise the change. | ||
We inherit from built-in classes, so that we don't need to change the existing | ||
code base to deal with the new types. | ||
We also should strive for immutability objects to avoid changes after the | ||
initial parsing. | ||
""" | ||
|
||
_mutated_: bool = False # TODO: Remove after deprecation warning is solved | ||
|
||
|
||
def _prevent_modification(target: type, method: str, copying: str) -> None: | ||
""" | ||
Because setuptools is very flexible we cannot fully prevent | ||
plugins and user customisations from modifying static values that were | ||
parsed from config files. | ||
But we can attempt to block "in-place" mutations and identify when they | ||
were done. | ||
""" | ||
fn = getattr(target, method, None) | ||
if fn is None: | ||
return | ||
|
||
@wraps(fn) | ||
def _replacement(self: Static, *args, **kwargs): | ||
# TODO: After deprecation period raise NotImplementedError instead of warning | ||
# which obviated the existence and checks of the `_mutated_` attribute. | ||
self._mutated_ = True | ||
SetuptoolsDeprecationWarning.emit( | ||
"Direct modification of value will be disallowed", | ||
f""" | ||
In an effort to implement PEP 643, direct/in-place changes of static values | ||
that come from configuration files are deprecated. | ||
If you need to modify this value, please first create a copy with {copying} | ||
and make sure conform to all relevant standards when overriding setuptools | ||
functionality (https://packaging.python.org/en/latest/specifications/). | ||
""", | ||
due_date=(2025, 10, 10), # Initially introduced in 2024-09-06 | ||
) | ||
return fn(self, *args, **kwargs) | ||
|
||
_replacement.__doc__ = "" # otherwise doctest may fail. | ||
setattr(target, method, _replacement) | ||
|
||
|
||
class Str(str, Static): | ||
pass | ||
|
||
|
||
class Tuple(tuple, Static): | ||
pass | ||
|
||
|
||
class List(list, Static): | ||
""" | ||
:meta private: | ||
>>> x = List([1, 2, 3]) | ||
>>> is_static(x) | ||
True | ||
>>> x += [0] # doctest: +IGNORE_EXCEPTION_DETAIL | ||
Traceback (most recent call last): | ||
SetuptoolsDeprecationWarning: Direct modification ... | ||
>>> is_static(x) # no longer static after modification | ||
False | ||
>>> y = list(x) | ||
>>> y.clear() | ||
>>> y | ||
[] | ||
>>> y == x | ||
False | ||
>>> is_static(List(y)) | ||
True | ||
""" | ||
|
||
|
||
# Make `List` immutable-ish | ||
# (certain places of setuptools/distutils issue a warn if we use tuple instead of list) | ||
for _method in ( | ||
'__delitem__', | ||
'__iadd__', | ||
'__setitem__', | ||
'append', | ||
'clear', | ||
'extend', | ||
'insert', | ||
'remove', | ||
'reverse', | ||
'pop', | ||
): | ||
_prevent_modification(List, _method, "`list(value)`") | ||
|
||
|
||
class Dict(dict, Static): | ||
""" | ||
:meta private: | ||
>>> x = Dict({'a': 1, 'b': 2}) | ||
>>> is_static(x) | ||
True | ||
>>> x['c'] = 0 # doctest: +IGNORE_EXCEPTION_DETAIL | ||
Traceback (most recent call last): | ||
SetuptoolsDeprecationWarning: Direct modification ... | ||
>>> x._mutated_ | ||
True | ||
>>> is_static(x) # no longer static after modification | ||
False | ||
>>> y = dict(x) | ||
>>> y.popitem() | ||
('b', 2) | ||
>>> y == x | ||
False | ||
>>> is_static(Dict(y)) | ||
True | ||
""" | ||
|
||
|
||
# Make `Dict` immutable-ish (we cannot inherit from types.MappingProxyType): | ||
for _method in ( | ||
'__delitem__', | ||
'__ior__', | ||
'__setitem__', | ||
'clear', | ||
'pop', | ||
'popitem', | ||
'setdefault', | ||
'update', | ||
): | ||
_prevent_modification(Dict, _method, "`dict(value)`") | ||
|
||
|
||
class SpecifierSet(packaging.specifiers.SpecifierSet, Static): | ||
"""Not exactly a built-in type but useful for ``requires-python``""" | ||
|
||
|
||
T = TypeVar("T") | ||
|
||
|
||
def noop(value: T) -> T: | ||
""" | ||
>>> noop(42) | ||
42 | ||
""" | ||
return value | ||
|
||
|
||
_CONVERSIONS = {str: Str, tuple: Tuple, list: List, dict: Dict} | ||
|
||
|
||
def attempt_conversion(value: T) -> T: | ||
""" | ||
>>> is_static(attempt_conversion("hello")) | ||
True | ||
>>> is_static(object()) | ||
False | ||
""" | ||
return _CONVERSIONS.get(type(value), noop)(value) # type: ignore[call-overload] | ||
|
||
|
||
def is_static(value: Any) -> bool: | ||
""" | ||
>>> is_static(a := Dict({'a': 1})) | ||
True | ||
>>> is_static(dict(a)) | ||
False | ||
>>> is_static(b := List([1, 2, 3])) | ||
True | ||
>>> is_static(list(b)) | ||
False | ||
""" | ||
return isinstance(value, Static) and not value._mutated_ | ||
|
||
|
||
EMPTY_LIST = List() | ||
EMPTY_DICT = Dict() |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An object parameter should match anything. You should be able to avoid using
Any
here.You can also make this function a type-guard with:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
In 72c4222, I went with the first approach suggested because I don't want the type checker to change the type it sees in a variable. For example if a variable is
_static.Str | str
, I would like the typechecker to keep considering it_static.Str | str
...Later if we need, we can change it to a
TypeGuard
.