feat(assessment): auto comprehensive assessment - rules engine from college scoring standard, per-semester stats, manual overrides, word export
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# Copyright 2010-2025 Antonio Cuni
|
||||
# Daniel Hahler
|
||||
#
|
||||
# All Rights Reserved
|
||||
"""Colorful tab completion for Python prompt"""
|
||||
from __future__ import annotations
|
||||
|
||||
from _colorize import ANSIColors, get_colors, get_theme
|
||||
import rlcompleter
|
||||
import keyword
|
||||
import types
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from _colorize import Theme
|
||||
|
||||
|
||||
def safe_getattr(obj, name):
|
||||
# Mirror rlcompleter's safeguards so completion does not
|
||||
# call properties or reify lazy module attributes.
|
||||
if isinstance(getattr(type(obj), name, None), property):
|
||||
return None
|
||||
if (isinstance(obj, types.ModuleType)
|
||||
and isinstance(obj.__dict__.get(name), types.LazyImportType)
|
||||
):
|
||||
return obj.__dict__.get(name)
|
||||
return getattr(obj, name, None)
|
||||
|
||||
|
||||
def colorize_matches(names: list[str], values: list[Any], theme: Theme) -> list[str]:
|
||||
return [
|
||||
_color_for_obj(name, obj, theme)
|
||||
for name, obj in zip(names, values)
|
||||
]
|
||||
|
||||
def _color_for_obj(name: str, value: Any, theme: Theme) -> str:
|
||||
t = type(value)
|
||||
color = _color_by_type(t, theme)
|
||||
return f"{color}{name}{ANSIColors.RESET}"
|
||||
|
||||
|
||||
def _color_by_type(t, theme):
|
||||
typename = t.__name__
|
||||
# this is needed e.g. to turn method-wrapper into method_wrapper,
|
||||
# because if we want _colorize.FancyCompleter to be "dataclassable"
|
||||
# our keys need to be valid identifiers.
|
||||
typename = typename.replace('-', '_').replace('.', '_')
|
||||
return getattr(theme.fancycompleter, typename, ANSIColors.RESET)
|
||||
|
||||
|
||||
class Completer(rlcompleter.Completer):
|
||||
"""
|
||||
When doing something like a.b.<tab>, keep the full a.b.attr completion
|
||||
stem so readline-style completion can keep refining the menu as you type.
|
||||
|
||||
Optionally, display the various completions in different colors
|
||||
depending on the type.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
namespace=None,
|
||||
*,
|
||||
use_colors='auto',
|
||||
consider_getitems=True,
|
||||
):
|
||||
from _pyrepl import readline
|
||||
rlcompleter.Completer.__init__(self, namespace)
|
||||
if use_colors == 'auto':
|
||||
# use colors only if we can
|
||||
use_colors = get_colors().RED != ""
|
||||
self.use_colors = use_colors
|
||||
self.consider_getitems = consider_getitems
|
||||
|
||||
if self.use_colors:
|
||||
# In GNU readline, this prevents escaping of ANSI control
|
||||
# characters in completion results. pyrepl's parse_and_bind()
|
||||
# is a no-op, but pyrepl handles ANSI sequences natively
|
||||
# via real_len()/stripcolor().
|
||||
readline.parse_and_bind('set dont-escape-ctrl-chars on')
|
||||
self.theme = get_theme()
|
||||
else:
|
||||
self.theme = None
|
||||
|
||||
if self.consider_getitems:
|
||||
delims = readline.get_completer_delims()
|
||||
delims = delims.replace('[', '')
|
||||
delims = delims.replace(']', '')
|
||||
readline.set_completer_delims(delims)
|
||||
|
||||
def complete(self, text, state):
|
||||
# if you press <tab> at the beginning of a line, insert an actual
|
||||
# \t. Else, trigger completion.
|
||||
if text == "":
|
||||
return ('\t', None)[state]
|
||||
else:
|
||||
return rlcompleter.Completer.complete(self, text, state)
|
||||
|
||||
def _callable_postfix(self, val, word):
|
||||
# disable automatic insertion of '(' for global callables
|
||||
return word
|
||||
|
||||
def _callable_attr_postfix(self, val, word):
|
||||
return rlcompleter.Completer._callable_postfix(self, val, word)
|
||||
|
||||
def global_matches(self, text):
|
||||
names = rlcompleter.Completer.global_matches(self, text)
|
||||
prefix = commonprefix(names)
|
||||
if prefix and prefix != text:
|
||||
return [prefix]
|
||||
|
||||
names.sort()
|
||||
values = []
|
||||
for name in names:
|
||||
clean_name = name.rstrip(': ')
|
||||
if keyword.iskeyword(clean_name) or keyword.issoftkeyword(clean_name):
|
||||
values.append(None)
|
||||
else:
|
||||
try:
|
||||
values.append(eval(name, self.namespace))
|
||||
except Exception:
|
||||
values.append(None)
|
||||
if self.use_colors and names:
|
||||
return self.colorize_matches(names, values)
|
||||
return names
|
||||
|
||||
def attr_matches(self, text):
|
||||
try:
|
||||
expr, attr, names, values = self._attr_matches(text)
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
if not names:
|
||||
return []
|
||||
|
||||
if len(names) == 1:
|
||||
# No coloring: when returning a single completion, readline
|
||||
# inserts it directly into the prompt, so ANSI codes would
|
||||
# appear as literal characters.
|
||||
return [self._callable_attr_postfix(values[0], f'{expr}.{names[0]}')]
|
||||
|
||||
prefix = commonprefix(names)
|
||||
if prefix and prefix != attr:
|
||||
return [f'{expr}.{prefix}'] # autocomplete prefix
|
||||
|
||||
names = [f'{expr}.{name}' for name in names]
|
||||
if self.use_colors:
|
||||
return self.colorize_matches(names, values)
|
||||
return names
|
||||
|
||||
def _attr_matches(self, text):
|
||||
expr, attr = text.rsplit('.', 1)
|
||||
if '(' in expr or ')' in expr: # don't call functions
|
||||
return expr, attr, [], []
|
||||
try:
|
||||
thisobject = eval(expr, self.namespace)
|
||||
except Exception:
|
||||
return expr, attr, [], []
|
||||
|
||||
# get the content of the object, except __builtins__
|
||||
words = set(dir(thisobject)) - {'__builtins__'}
|
||||
|
||||
if hasattr(thisobject, '__class__'):
|
||||
words.add('__class__')
|
||||
words.update(rlcompleter.get_class_members(thisobject.__class__))
|
||||
names = []
|
||||
values = []
|
||||
n = len(attr)
|
||||
if attr == '':
|
||||
noprefix = '_'
|
||||
elif attr == '_':
|
||||
noprefix = '__'
|
||||
else:
|
||||
noprefix = None
|
||||
|
||||
# sort the words now to make sure to return completions in
|
||||
# alphabetical order. It's easier to do it now, else we would need to
|
||||
# sort 'names' later but make sure that 'values' in kept in sync,
|
||||
# which is annoying.
|
||||
words = sorted(words)
|
||||
while True:
|
||||
for word in words:
|
||||
if (
|
||||
word[:n] == attr
|
||||
and not (noprefix and word[:n+1] == noprefix)
|
||||
):
|
||||
value = safe_getattr(thisobject, word)
|
||||
names.append(word)
|
||||
values.append(value)
|
||||
if names or not noprefix:
|
||||
break
|
||||
if noprefix == '_':
|
||||
noprefix = '__'
|
||||
else:
|
||||
noprefix = None
|
||||
|
||||
return expr, attr, names, values
|
||||
|
||||
def colorize_matches(self, names, values):
|
||||
return colorize_matches(names, values, self.theme)
|
||||
|
||||
|
||||
def commonprefix(names):
|
||||
"""Return the common prefix of all 'names'"""
|
||||
if not names:
|
||||
return ''
|
||||
s1 = min(names)
|
||||
s2 = max(names)
|
||||
for i, c in enumerate(s1):
|
||||
if c != s2[i]:
|
||||
return s1[:i]
|
||||
return s1
|
||||
Reference in New Issue
Block a user