autospec.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. # mautrix-instagram - A Matrix-Instagram puppeting bridge.
  2. # Copyright (C) 2020 Tulir Asokan
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. from typing import Tuple, Union
  17. import sys
  18. import attr
  19. from .type import TType
  20. TYPE_META = "net.maunium.instagram.thrift.type"
  21. if sys.version_info >= (3, 7):
  22. def _get_type_class(typ):
  23. try:
  24. return typ.__origin__
  25. except AttributeError:
  26. return None
  27. else:
  28. def _get_type_class(typ):
  29. try:
  30. return typ.__extra__
  31. except AttributeError:
  32. return None
  33. Subtype = Union[None, TType, Tuple['Subtype', 'Subtype']]
  34. def _guess_type(python_type, name: str) -> Tuple[TType, Subtype]:
  35. if python_type == str or python_type == bytes:
  36. return TType.BINARY, None
  37. elif python_type == bool:
  38. return TType.BOOL, None
  39. elif python_type == int:
  40. raise ValueError(f"Ambiguous integer field {name}")
  41. elif python_type == float:
  42. return TType.DOUBLE, None
  43. elif attr.has(python_type):
  44. return TType.STRUCT, None
  45. type_class = _get_type_class(python_type)
  46. args = getattr(python_type, "__args__", None)
  47. if type_class == list:
  48. return TType.LIST, _guess_type(args[0], f"{name} item")
  49. elif type_class == dict:
  50. return TType.MAP, (_guess_type(args[0], f"{name} key"),
  51. _guess_type(args[1], f"{name} value"))
  52. elif type_class == set:
  53. return TType.SET, _guess_type(args[0], f"{name} item")
  54. raise ValueError(f"Unknown type {python_type} for {name}")
  55. def autospec(clazz):
  56. """
  57. Automatically generate a thrift_spec dict based on attrs metadata.
  58. Args:
  59. clazz: The class to decorate.
  60. Returns:
  61. The class given as a parameter.
  62. """
  63. clazz.thrift_spec = {}
  64. index = 1
  65. for field in attr.fields(clazz):
  66. field_type, subtype = field.metadata.get(TYPE_META) or _guess_type(field.type, field.name)
  67. clazz.thrift_spec[index] = (field_type, field.name, subtype)
  68. index += 1
  69. return clazz
  70. def field(thrift_type: TType, subtype: Subtype = None, **kwargs) -> attr.Attribute:
  71. """
  72. Specify an explicit type for the :meth:`autospec` decorator.
  73. Args:
  74. thrift_type: The thrift type to use for the field.
  75. subtype: The subtype, for multi-part types like lists and maps.
  76. **kwargs: Other parameters to pass to :meth:`attr.ib`.
  77. Returns:
  78. The result of :meth:`attr.ib`
  79. """
  80. kwargs.setdefault("metadata", {})[TYPE_META] = (thrift_type, subtype)
  81. return attr.ib(**kwargs)