# -*- coding: utf-8 -*-
# Copyright 2019 - 2022 Avram Lubkin, All Rights Reserved
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
An pure Python implementation of tparm
Based on documentation
in man(5) terminfo
and comparing behavior of curses.tparm
"""
from collections
import deque
import operator
import re
OPERATORS = {b
'+': operator.add,
b
'-': operator.sub,
b
'*': operator.mul,
b
'/': operator.floordiv,
b
'm': operator.mod,
b
'&': operator.and_,
b
'|': operator.or_,
b
'^': operator.xor,
b
'=': operator.eq,
b
'>': operator.gt,
b
'<': operator.lt,
b
'~': operator.inv,
b
'!': operator.not_,
b
'A':
lambda x, y: bool(x
and y),
b
'O':
lambda x, y: bool(x
or y)}
FILTERS = ((
'_literal_percent', br
'%%'),
(
'_pop_c', br
'%c'),
(
'_increment_one_two', br
'%i'),
(
'_binary_op', br
'%([\+\-\*/m\&\|\^=><AO])'),
(
'_unary_op', br
'%([\~!])'),
(
'_push_param', br
'%p([1-9]\d*)'),
(
'_set_dynamic', br
'%P([a-z])'),
(
'_get_dynamic', br
'%g([a-z])'),
(
'_set_static', br
'%P([A-Z])'),
(
'_get_static', br
'%g([A-Z])'),
(
'_char_constant', br
"%'(.)'"),
(
'_int_constant', br
'%{(\d+)}'),
(
'_push_len', br
'%l'),
(
'_cond_if', br
'%\?(.+?)(?=%t)'),
(
'_cond_then_else', br
'%t(.+?)(?=%e)'),
(
'_cond_then_fi', br
'%t(.+?)(?=%;)'),
(
'_cond_else', br
'%e(.+?)(?=%;|$)'),
(
'_cond_fi', br
'%;'),
(
'_printf', br
'%:?[^%]*?[doxXs]'),
(
'_unmatched', br
'%.'),
(
'_literal', br
'[^%]+'))
PATTERNS = tuple((re.compile(pattern), filter_)
for filter_, pattern
in FILTERS)
NULL = type(
'Null', (int,), {})(0)
class TParm(object):
# pylint: disable=useless-object-inheritance
"""
Class to hold tparm methods
and persist variables between calls
"""
def __init__(self, *params, **kwargs):
self.rtn = b
''
self.stack = deque()
# The spec for tparm allows c string parameters, but most implementations don't
# The reference code makes a best effort to determine which parameters require strings
# We'll allow them without trying to predict
for param
in params:
if not isinstance(param, (int, bytes)):
raise TypeError(
'Parameters must be integers or bytes, not %s' %
type(param).__name__)
self.params = list(params)
static = kwargs.get(
'static')
self.static = {}
if static
is None else static
self.dynamic = {}
def __call__(self, string, *params):
self.dynamic = {}
# As of ncurses 6.3, dynamic variables do not persist between calls
return self.child(*params).parse(string)
def _literal_percent(self, group):
# pylint: disable=unused-argument
"""
Literal percent sign
"""
self.rtn += b
'%'
def _pop_c(self, group):
# pylint: disable=unused-argument
"""
Return pop() like %c
in printf
"""
try:
value = self.stack.pop()
except IndexError:
value = NULL
# Treat null as 0x80
if value
is NULL:
value = 0x80
self.rtn += b
'%c' % value
def _increment_one_two(self, group):
# pylint: disable=unused-argument
"""
Add 1 to first two parameters
Missing parameters are treated
as 0
's
"""
for index
in (0, 1):
try:
self.params[index] += 1
except IndexError:
self.params.append(1)
def _binary_op(self, group):
"""
Perform a binary operation on the last two items on the stack
The order of evaluation
is the order the items were placed on the stack
"""
second_val = self.stack.pop()
self.stack.append(OPERATORS[group](self.stack.pop(), second_val))
def _unary_op(self, group):
"""
Perform a unary operation on the last item on the stack
"""
self.stack.append(OPERATORS[group](self.stack.pop()))
def _push_param(self, group):
"""
Push a parameter onto the stack
If the parameter
is missing, push Null
"""
try:
self.stack.append(self.params[int(group) - 1])
except IndexError:
self.stack.append(NULL)
def _set_dynamic(self, group):
"""
Set the a dynamic variable to pop()
"""
self.dynamic[group] = self.stack.pop()
def _get_dynamic(self, group):
"""
Push the value of a dynamic variable onto the stack
"""
self.stack.append(self.dynamic.get(group, NULL))
def _set_static(self, group):
"""
Set the a static variable to pop()
"""
self.static[group] = self.stack.pop()
def _get_static(self, group):
"""
Push the value of a static variable onto the stack
"""
self.stack.append(self.static.get(group, NULL))
def _char_constant(self, group):
"""
Push an character constant onto the stack
"""
self.stack.append(ord(group))
def _int_constant(self, group):
"""
Push an integer constant onto the stack
"""
self.stack.append(int(group))
def _push_len(self, group):
# pylint: disable=unused-argument
"""
Replace the last item on the stack
with its length
"""
self.stack.append(len(self.stack.pop()))
def _cond_if(self, group):
"""
Recursively evaluate the body of the
if statement
"""
self.parse(group)
def _cond_then_else(self, group):
"""
If the last item on the stack
is True,
recursively evaluate then statement
Do
not consume last item on stack
"""
if self.stack[-1]:
self.parse(group)
def _cond_then_fi(self, group):
"""
If the last item on the stack
is True,
recursively evaluate then statement
Always consume last item on stack
"""
if self.stack.pop():
self.parse(group)
def _cond_else(self, group):
"""
If the last item on the stack
is False,
recursively evaluate the both of the
else statement
Always consume last item on stack
"""
if not self.stack.pop():
self.parse(group)
def _cond_fi(self, group):
# pylint: disable=unused-argument
"""
End
if statement
"""
def _printf(self, group):
"""
Subset of printf-like formatting
"""
# : is an escape to prevent flags from being treated as % operators, ignore
# Python 2 returns as ':', Python 3 returns as 58
if group[1]
in (b
':', 58):
group = b
'%' + group[2:]
try:
value = self.stack.pop()
except IndexError:
value = NULL
# Treat null as empty string when string formatting
# Python 2 returns as 's', Python 3 returns as 115
if value
is NULL
and group[-1]
in (b
's', 115):
value = b
''
self.rtn += group % value
def _unmatched(self, group):
# pylint: disable=unused-argument
"""
Escape pattern
with no spec
is skipped
"""
def _literal(self, group):
"""
Anything
not prefaced
with a known pattern spec
is treated literally
"""
self.rtn += group
def parse(self, string):
"""
Parsing loop
Evaluate regex patterns
in order until a pattern
is matched
"""
if not isinstance(string, bytes):
raise TypeError(
"A bytes-like object is required, not '%s'" % type(string).__name__)
index = 0
length = len(string)
while index < length:
for filt, meth
in PATTERNS:
# pragma: no branch
match = re.match(filt, string[index:])
if match:
group = match.groups()[-1]
if match.groups()
else match.group(0)
getattr(self, meth)(group)
index += match.end()
break
return self.rtn
def child(self, *params):
"""
Return a new instance
with the same variables, but different parameters
"""
return self.__class__(*params, static=self.static, dynamic=self.dynamic)
tparm = TParm()
# pylint: disable=invalid-name
"""Reimplementation of :py:func:`curses.tparm`"""