_adapters.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import email.message
  2. import email.policy
  3. import re
  4. import textwrap
  5. from ._text import FoldedCase
  6. class RawPolicy(email.policy.EmailPolicy):
  7. def fold(self, name, value):
  8. folded = self.linesep.join(
  9. textwrap
  10. .indent(value, prefix=' ' * 8, predicate=lambda line: True)
  11. .lstrip()
  12. .splitlines()
  13. )
  14. return f'{name}: {folded}{self.linesep}'
  15. class Message(email.message.Message):
  16. r"""
  17. Specialized Message subclass to handle metadata naturally.
  18. Reads values that may have newlines in them and converts the
  19. payload to the Description.
  20. >>> msg_text = textwrap.dedent('''
  21. ... Name: Foo
  22. ... Version: 3.0
  23. ... License: blah
  24. ... de-blah
  25. ... <BLANKLINE>
  26. ... First line of description.
  27. ... Second line of description.
  28. ... <BLANKLINE>
  29. ... Fourth line!
  30. ... ''').lstrip().replace('<BLANKLINE>', '')
  31. >>> msg = Message(email.message_from_string(msg_text))
  32. >>> msg['Description']
  33. 'First line of description.\nSecond line of description.\n\nFourth line!\n'
  34. Message should render even if values contain newlines.
  35. >>> print(msg)
  36. Name: Foo
  37. Version: 3.0
  38. License: blah
  39. de-blah
  40. Description: First line of description.
  41. Second line of description.
  42. <BLANKLINE>
  43. Fourth line!
  44. <BLANKLINE>
  45. <BLANKLINE>
  46. """
  47. multiple_use_keys = set(
  48. map(
  49. FoldedCase,
  50. [
  51. 'Classifier',
  52. 'Obsoletes-Dist',
  53. 'Platform',
  54. 'Project-URL',
  55. 'Provides-Dist',
  56. 'Provides-Extra',
  57. 'Requires-Dist',
  58. 'Requires-External',
  59. 'Supported-Platform',
  60. 'Dynamic',
  61. ],
  62. )
  63. )
  64. """
  65. Keys that may be indicated multiple times per PEP 566.
  66. """
  67. def __new__(cls, orig: email.message.Message):
  68. res = super().__new__(cls)
  69. vars(res).update(vars(orig))
  70. return res
  71. def __init__(self, *args, **kwargs):
  72. self._headers = self._repair_headers()
  73. # suppress spurious error from mypy
  74. def __iter__(self):
  75. return super().__iter__()
  76. def __getitem__(self, item):
  77. """
  78. Override parent behavior to typical dict behavior.
  79. ``email.message.Message`` will emit None values for missing
  80. keys. Typical mappings, including this ``Message``, will raise
  81. a key error for missing keys.
  82. Ref python/importlib_metadata#371.
  83. """
  84. res = super().__getitem__(item)
  85. if res is None:
  86. raise KeyError(item)
  87. return res
  88. def _repair_headers(self):
  89. def redent(value):
  90. "Correct for RFC822 indentation"
  91. indent = ' ' * 8
  92. if not value or '\n' + indent not in value:
  93. return value
  94. return textwrap.dedent(indent + value)
  95. headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
  96. if self._payload:
  97. headers.append(('Description', self.get_payload()))
  98. self.set_payload('')
  99. return headers
  100. def as_string(self):
  101. return super().as_string(policy=RawPolicy())
  102. @property
  103. def json(self):
  104. """
  105. Convert PackageMetadata to a JSON-compatible format
  106. per PEP 0566.
  107. """
  108. def transform(key):
  109. value = self.get_all(key) if key in self.multiple_use_keys else self[key]
  110. if key == 'Keywords':
  111. value = re.split(r'\s+', value)
  112. tk = key.lower().replace('-', '_')
  113. return tk, value
  114. return dict(map(transform, map(FoldedCase, self)))