_ident.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. # Copyright (c) 2025-2026 Buf Technologies, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Python identifier and module path types for code generation."""
  15. from __future__ import annotations
  16. from dataclasses import dataclass, field
  17. from typing import final
  18. from protobuf._descriptors import DescEnum, DescExtension, DescFile, DescMessage
  19. from protobuf._sanitization import (
  20. escape_extension_name,
  21. escape_identifier,
  22. escape_proto_module_component,
  23. )
  24. @final
  25. @dataclass(frozen=True, order=True)
  26. class Module:
  27. """A Python module path used for import resolution.
  28. Attributes:
  29. path: The import path of the module. If it starts with a `.`,
  30. it is treated as relative to the generation root; otherwise
  31. it is a fully qualified import.
  32. """
  33. @classmethod
  34. def for_desc(
  35. cls, desc: DescFile, suffix: str, *, escape_with_hash: bool = False
  36. ) -> Module:
  37. """Derive a relative module path from a file descriptor.
  38. Intermediate path components are sanitized (Python keywords and
  39. the `_pb` suffix are escaped). The final component gets
  40. `suffix` appended.
  41. Args:
  42. desc: The file descriptor.
  43. suffix: Appended to the final path component, e.g.
  44. `"_pb"` turns `any.proto` into module `any_pb`.
  45. escape_with_hash: If true, the module name is escaped with a hash suffix to avoid
  46. potential collisions with other artifacts.
  47. Returns:
  48. A relative `Module`.
  49. Examples:
  50. `google/protobuf/any.proto` with suffix `"_pb"` produces
  51. `.google.protobuf.any_pb`.
  52. """
  53. parts = desc.name.removesuffix(".proto").split("/")
  54. sanitized = [
  55. escape_proto_module_component(part, escape_with_hash=escape_with_hash)
  56. for part in parts
  57. ]
  58. sanitized[-1] = sanitized[-1] + suffix
  59. return cls(f".{'.'.join(sanitized)}")
  60. path: str
  61. def ident(self, name: str, *, type_only: bool = False) -> Ident:
  62. """Return an identifier belonging to this module.
  63. Args:
  64. name: The symbol name.
  65. type_only: If true, the import is only emitted inside an
  66. `if TYPE_CHECKING:` block.
  67. Returns:
  68. A `Ident` linked to this module.
  69. """
  70. return Ident(name, self, type_only)
  71. def module(self, name: str) -> Module:
  72. """Return a child module.
  73. Args:
  74. name: The child module name.
  75. Returns:
  76. A new `Module` with `name` appended to this module's path.
  77. """
  78. return Module(f"{self.path}.{escape_identifier(name)}")
  79. @final
  80. @dataclass(frozen=True, order=True)
  81. class Ident:
  82. """A Python identifier with its owning module.
  83. Attributes:
  84. name: The symbol name.
  85. module: The module this identifier is imported from.
  86. type_only: If true, the import is only emitted inside an
  87. `if TYPE_CHECKING:` block.
  88. """
  89. @classmethod
  90. def for_desc(
  91. cls,
  92. desc: DescEnum | DescMessage | DescExtension | DescFile,
  93. *,
  94. type_only: bool = False,
  95. escape_module_with_hash: bool = False,
  96. ) -> Ident:
  97. """Derive an importable identifier from a descriptor.
  98. For `DescFile`, the returned name is the module for the file
  99. within its Module, (e.g. `baz_pb` in `.foo.bar`). For message, enum, and
  100. extension descriptors the name is the generated symbol.
  101. Args:
  102. desc: A file, message, enum, or extension descriptor.
  103. type_only: If true, the import is only emitted inside an
  104. `if TYPE_CHECKING:` block.
  105. escape_module_with_hash: If true, the module name is escaped with a hash suffix to avoid
  106. potential collisions with other artifacts.
  107. Returns:
  108. An `Ident` linked to the derived module.
  109. """
  110. if isinstance(desc, DescFile):
  111. module = Module.for_desc(
  112. desc, "_pb", escape_with_hash=escape_module_with_hash
  113. )
  114. parent, _, module_name = module.path.rpartition(".")
  115. if not parent:
  116. parent = "."
  117. return cls(module_name, Module(parent), type_only, _desc=desc)
  118. module = Module.for_desc(
  119. desc.file, "_pb", escape_with_hash=escape_module_with_hash
  120. )
  121. identifier = (
  122. desc.type_name
  123. if desc.file.proto.package == ""
  124. else desc.type_name.removeprefix(f"{desc.file.proto.package}.")
  125. )
  126. match desc:
  127. case DescEnum() | DescMessage():
  128. identifier = desc._local_qualname
  129. case DescExtension():
  130. identifier = escape_extension_name(desc.name)
  131. if desc.parent:
  132. identifier = f"{desc.parent._local_qualname}.{identifier}"
  133. return cls(identifier, module, type_only, _desc=desc)
  134. name: str
  135. module: Module
  136. type_only: bool = field(default=False, hash=False, compare=False)
  137. _desc: DescEnum | DescMessage | DescExtension | DescFile | None = field(
  138. default=None, repr=False, hash=False, compare=False
  139. )