Source code for bpc_utils.context
"""BPC conversion context."""
import abc
import unicodedata
import parso.tree
from .misc import UUID4Generator
from .typing import TYPE_CHECKING, TypeVar, final
if TYPE_CHECKING:
from .misc import Config
from .typing import Callable, Final, Linesep, Optional, Tuple
BaseContextType = TypeVar('BaseContextType', bound='BaseContext')
[docs]class BaseContext(abc.ABC):
"""Abstract base class for general conversion context."""
def __init__(self, node: 'parso.tree.NodeOrLeaf', config: 'Config', *,
indent_level: int = 0, raw: bool = False) -> None:
"""Initialize BaseContext.
Args:
node: parso AST
config (:class:`~bpc_utils.Config`): conversion configurations
indent_level: current indentation level
raw: raw processing flag
"""
#: Internal configurations.
self.config = config # type: Final[Config]
#: Indentation sequence.
self._indentation = config.indentation # type: Final[str] # type: ignore[attr-defined]
#: Final[:data:`~bpc_utils.Linesep`]: Line separator.
self._linesep = config.linesep # type: Final[Linesep] # type: ignore[attr-defined]
#: :pep:`8` compliant conversion flag.
self._pep8 = config.pep8 # type: Final[bool] # type: ignore[attr-defined]
#: Root node given by the ``node`` parameter.
self._root = node # type: Final[parso.tree.NodeOrLeaf]
#: Current indentation level.
self._indent_level = indent_level # type: Final[int]
#: UUID generator.
self._uuid_gen = UUID4Generator(dash=False) # type: Final[UUID4Generator]
#: Code before insertion point.
self._prefix = '' # type: str
#: Code after insertion point.
self._suffix = '' # type: str
#: Final converted result.
self._buffer = '' # type: str
#: Flag to indicate whether buffer is now :attr:`self._prefix <bpc_utils.BaseContext._prefix>`.
self._prefix_or_suffix = True # type: bool
#: Preceding node with the target expression, i.e. the *insertion point*.
self._node_before_expr = None # type: Optional[parso.tree.NodeOrLeaf]
self._walk(node) # traverse children
if raw:
self._buffer = self._prefix + self._suffix
else:
self._concat() # generate final result
[docs] @final
def __iadd__(self: BaseContextType, code: str) -> BaseContextType:
"""Support of the ``+=`` operator.
If :attr:`self._prefix_or_suffix <bpc_utils.BaseContext._prefix_or_suffix>` is :data:`True`,
then the ``code`` will be appended to :attr:`self._prefix <bpc_utils.BaseContext._prefix>`;
else it will be appended to :attr:`self._suffix <bpc_utils.BaseContext._suffix>`.
Args:
code: code string
Returns:
BaseContext: self
"""
if self._prefix_or_suffix:
self._prefix += code
else:
self._suffix += code
return self
[docs] @final
def __str__(self) -> str:
"""Returns a *stripped* version of :attr:`self._buffer <bpc_utils.BaseContext._buffer>`."""
return self._buffer.strip()
@final
@property
def string(self) -> str:
"""Returns conversion buffer (:attr:`self._buffer <bpc_utils.BaseContext._buffer>`)."""
return self._buffer
[docs] @final
def _walk(self, node: 'parso.tree.NodeOrLeaf') -> None:
"""Start traversing the AST module.
The method traverses through all *children* of ``node``. It first checks
if such child has the target expression. If so, it will toggle
:attr:`self._prefix_or_suffix <bpc_utils.BaseContext._prefix_or_suffix>`
(set to :data:`False`) and save the last previous child as
:attr:`self._node_before_expr <bpc_utils.BaseContext._node_before_expr>`.
Then it processes the child with :meth:`self._process <bpc_utils.BaseContext._process>`.
Args:
node: parso AST
"""
# process node
if isinstance(node, parso.tree.BaseNode):
last_node = None
for child in node.children:
if self._prefix_or_suffix and self.has_expr(child):
self._prefix_or_suffix = False
self._node_before_expr = last_node
self._process(child)
last_node = child
return
# preserve leaf node as is by default
self += node.get_code()
[docs] @final
def _process(self, node: 'parso.tree.NodeOrLeaf') -> None:
"""Recursively process parso AST.
All processing methods for a specific ``node`` type are defined as
``_process_{type}``. This method first checks if such processing
method exists. If so, it will call such method on the ``node``;
otherwise it will traverse through all *children* of ``node``, and perform
the same logic on each child.
Args:
node: parso AST
"""
func_name = '_process_%s' % node.type
if hasattr(self, func_name):
func = getattr(self, func_name) # type: Callable[[parso.tree.NodeOrLeaf], None]
func(node)
return
# process node
if isinstance(node, parso.tree.BaseNode):
for child in node.children:
self._process(child)
return
# preserve leaf node as is by default
self += node.get_code()
[docs] @abc.abstractmethod
def _concat(self) -> None:
"""Concatenate final string."""
raise NotImplementedError # pragma: no cover
[docs] @abc.abstractmethod
def has_expr(self, node: 'parso.tree.NodeOrLeaf') -> bool:
"""Check if node has the target expression.
Args:
node: parso AST
Returns:
whether ``node`` has the target expression
"""
raise NotImplementedError # pragma: no cover
[docs] @final
@staticmethod
def missing_newlines(prefix: str, suffix: str, expected: int, linesep: 'Linesep') -> int:
"""Count missing blank lines for code insertion given surrounding code.
Args:
prefix: preceding source code
suffix: succeeding source code
expected: number of expected blank lines
linesep (:data:`~bpc_utils.Linesep`): line separator
Returns:
number of blank lines to add
"""
current = 0
# count trailing newlines in `prefix`
if prefix:
for line in reversed(prefix.split(linesep)): # pragma: no branch
if line.strip():
break
current += 1
if current > 0: # keep a trailing newline in `prefix`
current -= 1
# count leading newlines in `suffix`
if suffix:
for line in suffix.split(linesep): # pragma: no branch
if line.strip():
break
current += 1
missing = expected - current
return max(missing, 0)
[docs] @final
@staticmethod
def normalize(name: str) -> str:
"""Normalize variable names.
This method normalizes variable names as described in `Python documentation about identifiers`_
and :pep:`3131`.
Args:
name: variable name as it appears in the source code
Returns:
normalized variable name
.. _Python documentation about identifiers:
https://docs.python.org/3/reference/lexical_analysis.html#identifiers
"""
return unicodedata.normalize('NFKC', name)
[docs] @final
@classmethod
def mangle(cls, cls_name: str, var_name: str) -> str:
"""Mangle variable names.
This method mangles variable names as described in `Python documentation about mangling`_
and further normalizes the mangled variable name through :meth:`~bpc_utils.BaseContext.normalize`.
Args:
cls_name: class name
var_name: variable name
Returns:
mangled and normalized variable name
.. _Python documentation about mangling: https://docs.python.org/3/reference/expressions.html#atom-identifiers
"""
# should only perform mangling if variable name begins with two or more underscores
# and does not end in two or more underscores
if not var_name.startswith('__') or var_name.endswith('__'):
name = var_name
else:
# perform mangling, remove leading underscores from the class name when inserting
class_name_stripped = cls_name.lstrip('_')
if class_name_stripped:
name = '_%(cls)s%(var)s' % {'cls': class_name_stripped, 'var': var_name}
else:
# if the class name consists only of underscores, do not mangle
name = var_name
return cls.normalize(name)
__all__ = ['BaseContext']