Permit me to add the following to the review posted by toolic:
Use Docstrings
I would add a docstring to function validate_brackets
describing its functionality.
Create a set
from closing
Update
I have done a little research into the implementation of the dict_keys
class an instance of which is created with opening = brackets.keys()
and checking c in opening
should be more or less equivalent to checking c in brackets
, which is to say efficient. So there appears to be no need to create a set from the keys. However, if we want to efficiently check to see whether brackets.values()
contains duplicates, then it seems to me we should create a set
from these values and verify that its length is the same as the number of keys. This should also speed up the check c in closing
. We can check to see if the set of keys and the set of values are disjoint by using opening.isdisjoint(closing)
(see the next section concerning validation).
Sample Validation of the brackets Argument
Things you might test for:
- The brackets argument is a non-empty
dict
instance.
brackets.values()
contains no duplicate characters. Although this condition is not strictly necessary, it is the norm and we will enforce it.
- The opening and closing characters should be disjoint.
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 frozenset
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.
I would also make the default value for the brackets argument to be None
for which a default dictionary that requires no validation will be provided.
Putting It All Together
With the above suggested changes, the final source could be:
#!/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 = frozenset(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)
Prints:
text: 'abc[xx]'
brackets: [], result: ValueError('brackets must be a non-empty dict instance')
brackets: {}, result: ValueError('brackets must be a non-empty dict instance')
brackets: {'[': ']', '(': ']'}, result: ValueError('Duplicate closing characters')
brackets: {'[': ']', '(': '['}, result: ValueError('Opening and closing characters are not disjoint')
brackets: {'[': 'x'}, result: False
brackets: None, result: True
typer
for the CLI. \$\endgroup\$