codekingpro/portable-devtools
115k
1from functools import wraps2from typing import TypeVar3 4import packaging.specifiers5 6from .warnings import SetuptoolsDeprecationWarning7 8 9class Static:10 """11 Wrapper for built-in object types that are allow setuptools to identify12 static core metadata (in opposition to ``Dynamic``, as defined :pep:`643`).13 14 The trick is to mark values with :class:`Static` when they come from15 ``pyproject.toml`` or ``setup.cfg``, so if any plugin overwrite the value16 with a built-in, setuptools will be able to recognise the change.17 18 We inherit from built-in classes, so that we don't need to change the existing19 code base to deal with the new types.20 We also should strive for immutability objects to avoid changes after the21 initial parsing.22 """23 24 _mutated_: bool = False # TODO: Remove after deprecation warning is solved25 26 27def _prevent_modification(target: type, method: str, copying: str) -> None:28 """29 Because setuptools is very flexible we cannot fully prevent30 plugins and user customizations from modifying static values that were31 parsed from config files.32 But we can attempt to block "in-place" mutations and identify when they33 were done.34 """35 fn = getattr(target, method, None)36 if fn is None:37 return38 39 @wraps(fn)40 def _replacement(self: Static, *args, **kwargs):41 # TODO: After deprecation period raise NotImplementedError instead of warning42 # which obviated the existence and checks of the `_mutated_` attribute.43 self._mutated_ = True44 SetuptoolsDeprecationWarning.emit(45 "Direct modification of value will be disallowed",46 f"""47 In an effort to implement PEP 643, direct/in-place changes of static values48 that come from configuration files are deprecated.49 If you need to modify this value, please first create a copy with {copying}50 and make sure conform to all relevant standards when overriding setuptools51 functionality (https://packaging.python.org/en/latest/specifications/).52 """,53 due_date=(2025, 10, 10), # Initially introduced in 2024-09-0654 )55 return fn(self, *args, **kwargs)56 57 _replacement.__doc__ = "" # otherwise doctest may fail.58 setattr(target, method, _replacement)59 60 61class Str(str, Static):62 pass63 64 65class Tuple(tuple, Static):66 pass67 68 69class List(list, Static):70 """71 :meta private:72 >>> x = List([1, 2, 3])73 >>> is_static(x)74 True75 >>> x += [0] # doctest: +IGNORE_EXCEPTION_DETAIL76 Traceback (most recent call last):77 SetuptoolsDeprecationWarning: Direct modification ...78 >>> is_static(x) # no longer static after modification79 False80 >>> y = list(x)81 >>> y.clear()82 >>> y83 []84 >>> y == x85 False86 >>> is_static(List(y))87 True88 """89 90 91# Make `List` immutable-ish92# (certain places of setuptools/distutils issue a warn if we use tuple instead of list)93for _method in (94 '__delitem__',95 '__iadd__',96 '__setitem__',97 'append',98 'clear',99 'extend',100 'insert',101 'remove',102 'reverse',103 'pop',104):105 _prevent_modification(List, _method, "`list(value)`")106 107 108class Dict(dict, Static):109 """110 :meta private:111 >>> x = Dict({'a': 1, 'b': 2})112 >>> is_static(x)113 True114 >>> x['c'] = 0 # doctest: +IGNORE_EXCEPTION_DETAIL115 Traceback (most recent call last):116 SetuptoolsDeprecationWarning: Direct modification ...117 >>> x._mutated_118 True119 >>> is_static(x) # no longer static after modification120 False121 >>> y = dict(x)122 >>> y.popitem()123 ('b', 2)124 >>> y == x125 False126 >>> is_static(Dict(y))127 True128 """129 130 131# Make `Dict` immutable-ish (we cannot inherit from types.MappingProxyType):132for _method in (133 '__delitem__',134 '__ior__',135 '__setitem__',136 'clear',137 'pop',138 'popitem',139 'setdefault',140 'update',141):142 _prevent_modification(Dict, _method, "`dict(value)`")143 144 145class SpecifierSet(packaging.specifiers.SpecifierSet, Static):146 """Not exactly a built-in type but useful for ``requires-python``"""147 148 149T = TypeVar("T")150 151 152def noop(value: T) -> T:153 """154 >>> noop(42)155 42156 """157 return value158 159 160_CONVERSIONS = {str: Str, tuple: Tuple, list: List, dict: Dict}161 162 163def attempt_conversion(value: T) -> T:164 """165 >>> is_static(attempt_conversion("hello"))166 True167 >>> is_static(object())168 False169 """170 return _CONVERSIONS.get(type(value), noop)(value) # type: ignore[call-overload]171 172 173def is_static(value: object) -> bool:174 """175 >>> is_static(a := Dict({'a': 1}))176 True177 >>> is_static(dict(a))178 False179 >>> is_static(b := List([1, 2, 3]))180 True181 >>> is_static(list(b))182 False183 """184 return isinstance(value, Static) and not value._mutated_185 186 187EMPTY_LIST = List()188EMPTY_DICT = Dict()189 