Source code
Revision control
Copy as Markdown
Other Tools
# -*- coding: utf-8 -*-
# Copyright 2019 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
"""
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', None)
self.static = {} if static is None else static
dynamic = kwargs.get('static', None)
self.dynamic = {} if dynamic is None else dynamic
def __call__(self, string, *params):
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`"""