_adapters.py 3.7 KB

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