The above validations are more easily performed when the closing characters are a set. This will also speed up the check c in closing. We will place the validation logic in a separate validate_brackets_dict function for greater readability. Since this function might be called repeatedly for the same brackets dictionary instance, we can cache previously-validated dictionaries in a dictionary whose key is a tuplefrozenset of the dictionary's items (since dictionaries are not hashable) and whose value is a tuple consisting of the opening and closing characters to be used.
#!/usr/bin/env python3
from pathlib import Path
import typer
from typing import Union, Dict # For older Python versions
default_brackets = {"[": "]", "{": "}", "(": ")"}
default_opening = default_brackets.keys()
default_closing = set(default_brackets.values())
validated_brackets_dict = {}
def validate_brackets_dict(brackets: Dict) -> tuple:
"""Validate the brackets dictionary and if valid
return a tuple of the opening and closing characters to be
used. Otherwise, an exception is raised."""
if not (isinstance(brackets, dict) and brackets):
raise ValueError('brackets must be a non-empty dict instance')
cache_key = tuplefrozenset(brackets.items()) # We cannot cache a dictionary
opening_closing = validated_brackets_dict.get(cache_key, None)
if opening_closing is None:
# Validating a brackets dictionary that we haven't seen before
opening, closing = brackets.keys(), set(brackets.values())
# Although ensuring non-duplicate closing brackets is not strictly necessary,
# it is certainly the norm. So we can consider the following
# check optional but desirable:
if len(closing) != len(brackets):
raise ValueError('Duplicate closing characters')
# Check for closing characters disjoint from opening
# characters:
if not opening.isdisjoint(closing):
raise ValueError('Opening and closing characters are not disjoint')
# Cache for possible next time:
opening_closing = (opening, closing)
validated_brackets_dict[cache_key] = opening_closing
return opening_closing
def validate_brackets(text: str, brackets: Union[Dict, None]) -> bool:
"""Determine if the input text argument contains balanced
parentheses. The optional brackets arguments is a dictionary
of "parentheses" key/value pairs to be used.
If brackets is None then a default dictionary is used."""
if brackets is None:
brackets = default_brackets
opening, closing = default_opening, default_closing
else:
opening, closing = validate_brackets_dict(brackets)
stack = []
for c in text:
if c in opening:
stack.append(c)
elif c in closing:
if not stack or c != brackets[stack[-1]]:
return False
stack.pop()
return not stack
def main(file: Path) -> None:
with open(file) as f:
text = f.read()
print('text:', repr(text))
print()
# Some test case for validation:
for brackets in (
[],
{},
{'[': ']','(': ']'},
{'[': ']','(': '['},
{'[': 'x'},
None
):
print('brackets:', repr(brackets), end='')
try:
result = validate_brackets(text, brackets)
except Exception as e:
result = e
print(', result:', repr(result))
if __name__ == "__main__":
typer.run(main)