Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

189 linhas
4.7 KiB

  1. from functools import wraps
  2. from typing import TypeVar
  3. import packaging.specifiers
  4. from .warnings import SetuptoolsDeprecationWarning
  5. class Static:
  6. """
  7. Wrapper for built-in object types that are allow setuptools to identify
  8. static core metadata (in opposition to ``Dynamic``, as defined :pep:`643`).
  9. The trick is to mark values with :class:`Static` when they come from
  10. ``pyproject.toml`` or ``setup.cfg``, so if any plugin overwrite the value
  11. with a built-in, setuptools will be able to recognise the change.
  12. We inherit from built-in classes, so that we don't need to change the existing
  13. code base to deal with the new types.
  14. We also should strive for immutability objects to avoid changes after the
  15. initial parsing.
  16. """
  17. _mutated_: bool = False # TODO: Remove after deprecation warning is solved
  18. def _prevent_modification(target: type, method: str, copying: str) -> None:
  19. """
  20. Because setuptools is very flexible we cannot fully prevent
  21. plugins and user customisations from modifying static values that were
  22. parsed from config files.
  23. But we can attempt to block "in-place" mutations and identify when they
  24. were done.
  25. """
  26. fn = getattr(target, method, None)
  27. if fn is None:
  28. return
  29. @wraps(fn)
  30. def _replacement(self: Static, *args, **kwargs):
  31. # TODO: After deprecation period raise NotImplementedError instead of warning
  32. # which obviated the existence and checks of the `_mutated_` attribute.
  33. self._mutated_ = True
  34. SetuptoolsDeprecationWarning.emit(
  35. "Direct modification of value will be disallowed",
  36. f"""
  37. In an effort to implement PEP 643, direct/in-place changes of static values
  38. that come from configuration files are deprecated.
  39. If you need to modify this value, please first create a copy with {copying}
  40. and make sure conform to all relevant standards when overriding setuptools
  41. functionality (https://packaging.python.org/en/latest/specifications/).
  42. """,
  43. due_date=(2025, 10, 10), # Initially introduced in 2024-09-06
  44. )
  45. return fn(self, *args, **kwargs)
  46. _replacement.__doc__ = "" # otherwise doctest may fail.
  47. setattr(target, method, _replacement)
  48. class Str(str, Static):
  49. pass
  50. class Tuple(tuple, Static):
  51. pass
  52. class List(list, Static):
  53. """
  54. :meta private:
  55. >>> x = List([1, 2, 3])
  56. >>> is_static(x)
  57. True
  58. >>> x += [0] # doctest: +IGNORE_EXCEPTION_DETAIL
  59. Traceback (most recent call last):
  60. SetuptoolsDeprecationWarning: Direct modification ...
  61. >>> is_static(x) # no longer static after modification
  62. False
  63. >>> y = list(x)
  64. >>> y.clear()
  65. >>> y
  66. []
  67. >>> y == x
  68. False
  69. >>> is_static(List(y))
  70. True
  71. """
  72. # Make `List` immutable-ish
  73. # (certain places of setuptools/distutils issue a warn if we use tuple instead of list)
  74. for _method in (
  75. '__delitem__',
  76. '__iadd__',
  77. '__setitem__',
  78. 'append',
  79. 'clear',
  80. 'extend',
  81. 'insert',
  82. 'remove',
  83. 'reverse',
  84. 'pop',
  85. ):
  86. _prevent_modification(List, _method, "`list(value)`")
  87. class Dict(dict, Static):
  88. """
  89. :meta private:
  90. >>> x = Dict({'a': 1, 'b': 2})
  91. >>> is_static(x)
  92. True
  93. >>> x['c'] = 0 # doctest: +IGNORE_EXCEPTION_DETAIL
  94. Traceback (most recent call last):
  95. SetuptoolsDeprecationWarning: Direct modification ...
  96. >>> x._mutated_
  97. True
  98. >>> is_static(x) # no longer static after modification
  99. False
  100. >>> y = dict(x)
  101. >>> y.popitem()
  102. ('b', 2)
  103. >>> y == x
  104. False
  105. >>> is_static(Dict(y))
  106. True
  107. """
  108. # Make `Dict` immutable-ish (we cannot inherit from types.MappingProxyType):
  109. for _method in (
  110. '__delitem__',
  111. '__ior__',
  112. '__setitem__',
  113. 'clear',
  114. 'pop',
  115. 'popitem',
  116. 'setdefault',
  117. 'update',
  118. ):
  119. _prevent_modification(Dict, _method, "`dict(value)`")
  120. class SpecifierSet(packaging.specifiers.SpecifierSet, Static):
  121. """Not exactly a built-in type but useful for ``requires-python``"""
  122. T = TypeVar("T")
  123. def noop(value: T) -> T:
  124. """
  125. >>> noop(42)
  126. 42
  127. """
  128. return value
  129. _CONVERSIONS = {str: Str, tuple: Tuple, list: List, dict: Dict}
  130. def attempt_conversion(value: T) -> T:
  131. """
  132. >>> is_static(attempt_conversion("hello"))
  133. True
  134. >>> is_static(object())
  135. False
  136. """
  137. return _CONVERSIONS.get(type(value), noop)(value) # type: ignore[call-overload]
  138. def is_static(value: object) -> bool:
  139. """
  140. >>> is_static(a := Dict({'a': 1}))
  141. True
  142. >>> is_static(dict(a))
  143. False
  144. >>> is_static(b := List([1, 2, 3]))
  145. True
  146. >>> is_static(list(b))
  147. False
  148. """
  149. return isinstance(value, Static) and not value._mutated_
  150. EMPTY_LIST = List()
  151. EMPTY_DICT = Dict()