autospec.py 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  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 attr
  18. from .type import TType
  19. TYPE_META = "fi.mau.instagram.thrift.type"
  20. def _get_type_class(typ):
  21. try:
  22. return typ.__origin__
  23. except AttributeError:
  24. return None
  25. Subtype = Union[None, TType, Tuple["Subtype", "Subtype"]]
  26. def _guess_type(python_type, name: str) -> Tuple[TType, Subtype]:
  27. if python_type == str or python_type == bytes:
  28. return TType.BINARY, None
  29. elif python_type == bool:
  30. return TType.BOOL, None
  31. elif python_type == int:
  32. raise ValueError(f"Ambiguous integer field {name}")
  33. elif python_type == float:
  34. return TType.DOUBLE, None
  35. elif attr.has(python_type):
  36. return TType.STRUCT, None
  37. type_class = _get_type_class(python_type)
  38. args = getattr(python_type, "__args__", None)
  39. if type_class == list:
  40. return TType.LIST, _guess_type(args[0], f"{name} item")
  41. elif type_class == dict:
  42. return TType.MAP, (
  43. _guess_type(args[0], f"{name} key"),
  44. _guess_type(args[1], f"{name} value"),
  45. )
  46. elif type_class == set:
  47. return TType.SET, _guess_type(args[0], f"{name} item")
  48. raise ValueError(f"Unknown type {python_type} for {name}")
  49. def autospec(clazz):
  50. """
  51. Automatically generate a thrift_spec dict based on attrs metadata.
  52. Args:
  53. clazz: The class to decorate.
  54. Returns:
  55. The class given as a parameter.
  56. """
  57. clazz.thrift_spec = {}
  58. index = 1
  59. for field in attr.fields(clazz):
  60. field_type, subtype = field.metadata.get(TYPE_META) or _guess_type(field.type, field.name)
  61. clazz.thrift_spec[index] = (field_type, field.name, subtype)
  62. index += 1
  63. return clazz
  64. def field(thrift_type: TType, subtype: Subtype = None, **kwargs) -> attr.Attribute:
  65. """
  66. Specify an explicit type for the :meth:`autospec` decorator.
  67. Args:
  68. thrift_type: The thrift type to use for the field.
  69. subtype: The subtype, for multi-part types like lists and maps.
  70. **kwargs: Other parameters to pass to :meth:`attr.ib`.
  71. Returns:
  72. The result of :meth:`attr.ib`
  73. """
  74. kwargs.setdefault("metadata", {})[TYPE_META] = (thrift_type, subtype)
  75. return attr.ib(**kwargs)