You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
12 KiB
Python

"""Helper functionality that is used in other modules of this package."""
from __future__ import annotations
from copy import copy
from dataclasses import dataclass
from dataclasses import field
from typing import Any
BUILDER_NAME = "Builder"
READER_NAME = "Reader"
def new_builder(type_name: str) -> str:
"""Converts a type name to its builder variant.
E.g. `MyClass` becomes `MyClassBuilder`.
Args:
type_name (str): The original type name.
Returns:
str: The builder variant.
"""
return f"{type_name}{BUILDER_NAME}"
def new_reader(type_name: str) -> str:
"""Converts a type name to its reader variant.
E.g. `MyClass` becomes `MyClassTeader`.
Args:
type_name (str): The original type name.
Returns:
str: The reader variant.
"""
return f"{type_name}{READER_NAME}"
@dataclass
class TypeHint:
"""A class that captures a type hint."""
name: str
scopes: list[str] = field(default_factory=list)
affix: str = ""
primary: bool = False
def __str__(self) -> str:
"""The string representation of the type hint.
This is composed of the scopes (if any), the name of the hint, and the affix (if any).
"""
if not self.scopes:
return f"{self.name}{self.affix}"
else:
return f"{'.'.join(self.scopes)}.{self.name}{self.affix}"
@dataclass
class TypeHintedVariable:
"""A class that represents a type hinted variable."""
name: str
type_hints: list[TypeHint]
default: str = ""
nesting_depth: int = 0
def __post_init__(self):
"""Sanity check for provided type hints."""
primary_type_hint_count = 0
for type_hint in self.type_hints:
if type_hint.primary:
primary_type_hint_count += 1
if primary_type_hint_count != 1:
raise ValueError(f"There can only be exactly one primary type hint. Found {primary_type_hint_count}")
def __str__(self) -> str:
"""String representation of this object.
Returns:
str: The string representation.
"""
return self.typed_variable_with_full_hints
def _nest(self, unnested_type_name: str) -> str:
if self.nesting_depth > 0:
return f'{self.nesting_depth * "Sequence["}{unnested_type_name}{self.nesting_depth * "]"}'
else:
return unnested_type_name
@property
def typed_variable_with_full_hints(self) -> str:
"""Returns the typed variable string, hinted will all available type hints."""
return self._generate_typed_variable(self.full_type)
@property
def typed_variable_with_primary_hint(self) -> str:
"""Returns the typed variable string, hinted only with the primary type hint."""
if self.primary_type_hint is None:
raise ValueError("No primary type hint available.")
return self._generate_typed_variable(str(self.primary_type_hint))
@property
def primary_type_hint(self) -> TypeHint:
"""Returns the primary type hint."""
for type_hint in self.type_hints:
if type_hint.primary:
return type_hint
raise RuntimeError("Primary type hint not found.")
def _generate_typed_variable(self, type_name: str) -> str:
"""Generate the typed variable string for a chosen type name.
Args:
type_name (str): The type name to use.
Returns:
str: The typed variable string.
"""
nested_type_name = self._nest(type_name)
typed_variable = f"{self.name}: {nested_type_name}"
if self.default:
return f"{typed_variable} = {self.default}"
else:
return typed_variable
def _get_type_hints_for_affixes(self, affixes: list[str]) -> list[TypeHint]:
return [self.get_type_hint_for_affix(affix) for affix in affixes]
def get_typed_variable_with_affixes(self, affixes: list[str] | None) -> str:
"""Gets a typed version of this variable with the selected type hint affixes.
For example, when selecting the affixes "Reader" and "Builder" for the primary type "Something" and the variable
named "VariableName", this will return "VariableName: SomethingReader | SomethingBuilder".
Args:
affixes (list[str] | None): The affixes to select for type hints.
Returns:
str: The typed variable in string format.
"""
type_hints: list[TypeHint] = []
if affixes is not None:
type_hints_for_affixes = self._get_type_hints_for_affixes(affixes)
if type_hints_for_affixes is not None:
type_hints.extend(type_hints_for_affixes)
return self._generate_typed_variable(self._join_type_hints(type_hints))
def _join_type_hints(self, type_hints: list[TypeHint]) -> str:
return " | ".join(str(type_hint) for type_hint in type_hints)
@property
def full_type(self) -> str:
"""The full type string of the hinted variable."""
return self._join_type_hints(self.type_hints)
def add_type_hint(self, new_type_hint: TypeHint):
"""Add a type hint to the hinted variable.
Args:
new_type_hint (TypeHint): The type hint to add.
"""
for type_hint in self.type_hints:
if type_hint == new_type_hint:
raise ValueError("Type hint already exists.")
if new_type_hint.primary:
raise ValueError("There can only be one primary type.")
self.type_hints.append(new_type_hint)
def add_type_scope(self, scope: str):
"""Add a scope to the type name.
Args:
scope (str): The scope name to prefix to the type name.
"""
for type_hint in self.type_hints:
type_hint.scopes.append(scope)
def get_type_hint_for_affix(self, affix: str) -> TypeHint:
"""Looks for a type hint that has the provided affix and returns it.
Args:
affix (str | None): The affix to look for.
Returns:
TypeHint: The type hint that was found.
"""
for type_hint in self.type_hints:
if type_hint.affix == affix:
return type_hint
raise KeyError(f"Affix '{affix}' is not present in any recorded type hint.")
def has_type_hint_with_affix(self, affix: str) -> bool:
"""Assess, whether or not the variable has a type hint with the provided affix."""
try:
self.get_type_hint_for_affix(affix)
except KeyError:
return False
else:
return True
@property
def has_type_hint_with_builder_affix(self) -> bool:
"""Whether the variable holds a type hint with a builder affix."""
return self.has_type_hint_with_affix(BUILDER_NAME)
@property
def has_type_hint_with_reader_affix(self) -> bool:
"""Whether the variable holds a type hint with a reader affix."""
return self.has_type_hint_with_affix(READER_NAME)
def add_builder_from_primary_type(self):
"""Add a type hint with builder affix, based on the primary type."""
self.add_type_hint(TypeHint(self.primary_type_hint.name, copy(self.primary_type_hint.scopes), BUILDER_NAME))
def add_reader_from_primary_type(self):
"""Add a type hint with builder affix, based on the primary type."""
self.add_type_hint(
TypeHint(
self.primary_type_hint.name,
copy(self.primary_type_hint.scopes),
READER_NAME,
)
)
def replace_capnp_suffix(original: str) -> str:
"""If found, replaces the .capnp suffix in a string with _capnp.
For example, `some_module.capnp` becomes `some_module_capnp`.
Args:
original (str): The string to replace the suffix in.
Returns:
str: The string with the replaced suffix.
"""
if original.endswith(".capnp"):
return original.replace(".capnp", "_capnp")
else:
return original
def join_parameters(parameters: list[TypeHintedVariable] | list[str] | None) -> str:
"""Joins parameters by means of ', '.
Args:
parameters (list[HintedVariable] | list[str] | None): The parameters to join.
Returns:
str: The joined parameters.
"""
if parameters:
return ", ".join(str(p) for p in parameters if p)
else:
return ""
def new_type_alias(alias: str, type_name: str) -> str:
"""Generate a string for a type-alias.
For example, for a type alias `SomeType` with a type `TheType` this gives `SomeType = TheType`.
Args:
alias (str): The alias name.
type_name (str): The aliased type.
Returns:
str: The type alias.
"""
return f"{alias} = {type_name}"
def new_group(name: str, members: list[str]) -> str:
"""Create a string for a group name and its members.
For example, when the group name is 'Type', and the parameters are 'str', and 'int',
the output will be 'Type[str, int]'.
Args:
name (str): The name of the group.
members (list[str]): The members of the group
Returns:
str: The resulting group string.
"""
return f"{name}[{join_parameters(members)}]"
def new_type_group(name: str, types: list[str]) -> str:
"""Create a string for a parameter with types.
Uses `new_group` internally.
Args:
name (str): The name of the parameter.
types (list[str]): The list of types to that this parameter can have.
Returns:
str: The resulting parameter string.
"""
return new_group(name, types)
def new_function(
name: str, parameters: list[TypeHintedVariable] | list[str] | None = None, return_type: str | None = None
) -> str:
"""Create a string for a function.
Args:
name (str): The function name.
parameters (list[HintedVariable] | list[str] | None, optional): The function parameters, if any. Defaults to None.
return_type (str | None, optional): The function's return type. Defaults to None.
Returns:
str: The function string.
"""
if return_type is None:
return_type = "None"
arguments = join_parameters(parameters)
return f"def {name}({arguments}) -> {return_type}: ..."
def new_decorator(name: str, parameters: list[TypeHintedVariable] | list[str] | None = None) -> str:
"""Create a new decorator.
Args:
name (str): The name of the decorator.
parameters (list[HintedVariable] | list[str] | None, optional): The parameters (args, kwargs) of the decorator,
if any. Defaults to None.
Returns:
str: The decorator string.
"""
if parameters:
return f"@{name}({join_parameters(parameters)})"
else:
return f"@{name}"
def new_constructor(kwargs: list[str] | None = None) -> str:
"""Creates a new constructor.
Args:
kwargs (list[str] | None, optional): The keyword arguments for the constructor. Defaults to None.
Returns:
str: The constructor string.
"""
constructor_args = "self"
kwargs_ = ""
if kwargs:
# Prepend '*' for catching any positional arguments, before the keyword arguments.
kwargs_ = join_parameters(["*"] + [f"{kwarg} = ..." for kwarg in kwargs])
return new_function("__init__", parameters=[constructor_args, kwargs_])
def new_class_declaration(name: str, parameters: list[str] | None = None) -> str:
"""Creates a string for declaring a class.
For example, for a name of 'SomeClass' and a list of parameters that is 'str, Type[str, int]', the output
will be 'SomeClass (str, Type[str, int]):'.
If no parameters are provided, the output is just 'SomeClass:'.
Args:
name (str): The class name.
parameters (list[str] | None, optional):
A list of parameters that are part of the class declaration. Defaults to None.
Returns:
str: The class declaration.
"""
if parameters:
return f"class {name}({join_parameters(parameters)}):"
else:
return f"class {name}:"
def get_display_name(schema: Any) -> str:
"""Extract the display name from a schema.
Args:
schema (Any): The schema to get the display name from.
Returns:
str: The display name of the schema.
"""
return schema.node.displayName[schema.node.displayNamePrefixLength :]