| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- # Copyright (c) 2025-2026 Buf Technologies, Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """Python identifier and module path types for code generation."""
- from __future__ import annotations
- from dataclasses import dataclass, field
- from typing import final
- from protobuf._descriptors import DescEnum, DescExtension, DescFile, DescMessage
- from protobuf._sanitization import (
- escape_extension_name,
- escape_identifier,
- escape_proto_module_component,
- )
- @final
- @dataclass(frozen=True, order=True)
- class Module:
- """A Python module path used for import resolution.
- Attributes:
- path: The import path of the module. If it starts with a `.`,
- it is treated as relative to the generation root; otherwise
- it is a fully qualified import.
- """
- @classmethod
- def for_desc(
- cls, desc: DescFile, suffix: str, *, escape_with_hash: bool = False
- ) -> Module:
- """Derive a relative module path from a file descriptor.
- Intermediate path components are sanitized (Python keywords and
- the `_pb` suffix are escaped). The final component gets
- `suffix` appended.
- Args:
- desc: The file descriptor.
- suffix: Appended to the final path component, e.g.
- `"_pb"` turns `any.proto` into module `any_pb`.
- escape_with_hash: If true, the module name is escaped with a hash suffix to avoid
- potential collisions with other artifacts.
- Returns:
- A relative `Module`.
- Examples:
- `google/protobuf/any.proto` with suffix `"_pb"` produces
- `.google.protobuf.any_pb`.
- """
- parts = desc.name.removesuffix(".proto").split("/")
- sanitized = [
- escape_proto_module_component(part, escape_with_hash=escape_with_hash)
- for part in parts
- ]
- sanitized[-1] = sanitized[-1] + suffix
- return cls(f".{'.'.join(sanitized)}")
- path: str
- def ident(self, name: str, *, type_only: bool = False) -> Ident:
- """Return an identifier belonging to this module.
- Args:
- name: The symbol name.
- type_only: If true, the import is only emitted inside an
- `if TYPE_CHECKING:` block.
- Returns:
- A `Ident` linked to this module.
- """
- return Ident(name, self, type_only)
- def module(self, name: str) -> Module:
- """Return a child module.
- Args:
- name: The child module name.
- Returns:
- A new `Module` with `name` appended to this module's path.
- """
- return Module(f"{self.path}.{escape_identifier(name)}")
- @final
- @dataclass(frozen=True, order=True)
- class Ident:
- """A Python identifier with its owning module.
- Attributes:
- name: The symbol name.
- module: The module this identifier is imported from.
- type_only: If true, the import is only emitted inside an
- `if TYPE_CHECKING:` block.
- """
- @classmethod
- def for_desc(
- cls,
- desc: DescEnum | DescMessage | DescExtension | DescFile,
- *,
- type_only: bool = False,
- escape_module_with_hash: bool = False,
- ) -> Ident:
- """Derive an importable identifier from a descriptor.
- For `DescFile`, the returned name is the module for the file
- within its Module, (e.g. `baz_pb` in `.foo.bar`). For message, enum, and
- extension descriptors the name is the generated symbol.
- Args:
- desc: A file, message, enum, or extension descriptor.
- type_only: If true, the import is only emitted inside an
- `if TYPE_CHECKING:` block.
- escape_module_with_hash: If true, the module name is escaped with a hash suffix to avoid
- potential collisions with other artifacts.
- Returns:
- An `Ident` linked to the derived module.
- """
- if isinstance(desc, DescFile):
- module = Module.for_desc(
- desc, "_pb", escape_with_hash=escape_module_with_hash
- )
- parent, _, module_name = module.path.rpartition(".")
- if not parent:
- parent = "."
- return cls(module_name, Module(parent), type_only, _desc=desc)
- module = Module.for_desc(
- desc.file, "_pb", escape_with_hash=escape_module_with_hash
- )
- identifier = (
- desc.type_name
- if desc.file.proto.package == ""
- else desc.type_name.removeprefix(f"{desc.file.proto.package}.")
- )
- match desc:
- case DescEnum() | DescMessage():
- identifier = desc._local_qualname
- case DescExtension():
- identifier = escape_extension_name(desc.name)
- if desc.parent:
- identifier = f"{desc.parent._local_qualname}.{identifier}"
- return cls(identifier, module, type_only, _desc=desc)
- name: str
- module: Module
- type_only: bool = field(default=False, hash=False, compare=False)
- _desc: DescEnum | DescMessage | DescExtension | DescFile | None = field(
- default=None, repr=False, hash=False, compare=False
- )
|