| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- from functools import wraps
- from typing import 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 customizations 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: object) -> 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()
|