debugpy/tests/patterns/_impl.py
2022-04-13 11:38:41 -07:00

387 lines
10 KiB
Python

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
# The actual patterns are defined here, so that tests.patterns.some can redefine
# builtin names like str, int etc without affecting the implementations in this
# file - some.* then provides shorthand aliases.
import collections
import itertools
import py.path
import re
import sys
from debugpy.common import util
import pydevd_file_utils
class Some(object):
"""A pattern that can be tested against a value with == to see if it matches."""
def matches(self, value):
raise NotImplementedError
def __repr__(self):
try:
return self.name
except AttributeError:
raise NotImplementedError
def __eq__(self, value):
return self.matches(value)
def __ne__(self, value):
return not self.matches(value)
def __invert__(self):
"""The inverse pattern - matches everything that this one doesn't."""
return Not(self)
def __or__(self, pattern):
"""Union pattern - matches if either of the two patterns match."""
return Either(self, pattern)
def such_that(self, condition):
"""Same pattern, but it only matches if condition() is true."""
return SuchThat(self, condition)
def in_range(self, start, stop):
"""Same pattern, but it only matches if the start <= value < stop."""
return InRange(self, start, stop)
def equal_to(self, obj):
return EqualTo(self, obj)
def not_equal_to(self, obj):
return NotEqualTo(self, obj)
def same_as(self, obj):
return SameAs(self, obj)
def matching(self, regex, flags=0):
"""Same pattern, but it only matches if re.match(regex, flags) produces
a match that corresponds to the entire string.
"""
return Matching(self, regex, flags)
# Used to obtain the JSON representation for logging. This is a hack, because
# JSON serialization doesn't allow to customize raw output - this function can
# only substitute for another object that is normally JSON-serializable. But
# for patterns, we want <...> in the logs, not'"<...>". Thus, we insert dummy
# marker chars here, such that it looks like "\002<...>\003" in serialized JSON -
# and then tests.timeline._describe_message does a string substitution on the
# result to strip out '"\002' and '\003"'.
def __getstate__(self):
return "\002" + repr(self) + "\003"
class Not(Some):
"""Matches the inverse of the pattern."""
def __init__(self, pattern):
self.pattern = pattern
def __repr__(self):
return f"~{self.pattern!r}"
def matches(self, value):
return value != self.pattern
class Either(Some):
"""Matches either of the patterns."""
def __init__(self, *patterns):
assert len(patterns) > 0
self.patterns = tuple(patterns)
def __repr__(self):
try:
return self.name
except AttributeError:
return "({0})".format(" | ".join(repr(pat) for pat in self.patterns))
def matches(self, value):
return any(pattern == value for pattern in self.patterns)
def __or__(self, pattern):
return Either(*(self.patterns + (pattern,)))
class Object(Some):
"""Matches anything."""
name = "<?>"
def matches(self, value):
return True
class Thing(Some):
"""Matches anything that is not None."""
name = "<>"
def matches(self, value):
return value is not None
class InstanceOf(Some):
"""Matches any object that is an instance of the specified type."""
def __init__(self, classinfo, name=None):
if isinstance(classinfo, type):
classinfo = (classinfo,)
assert len(classinfo) > 0 and all(
(isinstance(cls, type) for cls in classinfo)
), "classinfo must be a type or a tuple of types"
self.name = name
self.classinfo = classinfo
def __repr__(self):
if self.name:
name = self.name
else:
name = " | ".join(cls.__name__ for cls in self.classinfo)
return f"<{name}>"
def matches(self, value):
return isinstance(value, self.classinfo)
class Path(Some):
"""Matches any string that matches the specified path.
Uses os.path.normcase() to normalize both strings before comparison.
"""
def __init__(self, path):
if isinstance(path, py.path.local):
path = path.strpath
assert isinstance(path, str)
self.path = path
def __repr__(self):
return "path({self.path!r})"
def __str__(self):
return self.path
def __getstate__(self):
return self.path
def matches(self, other):
if isinstance(other, py.path.local):
other = other.strpath
if isinstance(other, str):
pass
elif isinstance(other, bytes):
other = other.encode(sys.getfilesystemencoding())
else:
return NotImplemented
left = pydevd_file_utils.get_path_with_real_case(self.path)
right = pydevd_file_utils.get_path_with_real_case(other)
return left == right
class ListContaining(Some):
"""Matches any list that contains the specified subsequence of elements."""
def __init__(self, *items):
self.items = tuple(items)
def __repr__(self):
if not self.items:
return "[...]"
s = repr(list(self.items))
return f"[..., {s[1:-1]}, ...]"
def __getstate__(self):
items = ["\002...\003"]
if not self.items:
return items
items *= 2
items[1:1] = self.items
return items
def matches(self, other):
if not isinstance(other, list):
return NotImplemented
items = self.items
if not items:
return True # every list contains an empty sequence
if len(items) == 1:
return self.items[0] in other
# Zip the other list with itself, shifting by one every time, to produce
# tuples of equal length with items - i.e. all potential subsequences. So,
# given other=[1, 2, 3, 4, 5] and items=(2, 3, 4), we want to get a list
# like [(1, 2, 3), (2, 3, 4), (3, 4, 5)] - and then search for items in it.
iters = [itertools.islice(other, i, None) for i in range(0, len(items))]
subseqs = zip(*iters)
return any(subseq == items for subseq in subseqs)
class DictContaining(Some):
"""Matches any dict that contains the specified key-value pairs::
d1 = {'a': 1, 'b': 2, 'c': 3}
d2 = {'a': 1, 'b': 2}
assert d1 == some.dict.containing(d2)
assert d2 != some.dict.containing(d1)
"""
def __init__(self, items):
self.items = collections.OrderedDict(items)
def __repr__(self):
return dict.__repr__(self.items)[:-1] + ", ...}"
def __getstate__(self):
items = self.items.copy()
items["\002..."] = "...\003"
return items
def matches(self, other):
if not isinstance(other, dict):
return NotImplemented
any = Object()
d = {key: any for key in other}
d.update(self.items)
return d == other
class Also(Some):
"""Base class for patterns that narrow down another pattern."""
def __init__(self, pattern):
self.pattern = pattern
def matches(self, value):
return self.pattern == value and self._also(value)
def _also(self, value):
raise NotImplementedError
class SuchThat(Also):
"""Matches only if condition is true."""
def __init__(self, pattern, condition):
super().__init__(pattern)
self.condition = condition
def __repr__(self):
try:
return self.name
except AttributeError:
return f"({self.pattern!r} if {util.nameof(self.condition)})"
def _also(self, value):
return self.condition(value)
class InRange(Also):
"""Matches only if the value is within the specified range."""
def __init__(self, pattern, start, stop):
super().__init__(pattern)
self.start = start
self.stop = stop
def __repr__(self):
try:
return self.name
except AttributeError:
return f"({self.start!r} <= {self.pattern!r} < {self.stop!r})"
def _also(self, value):
return self.start <= value < self.stop
class EqualTo(Also):
"""Matches any object that is equal to the specified object."""
def __init__(self, pattern, obj):
super().__init__(pattern)
self.obj = obj
def __repr__(self):
return repr(self.obj)
def __str__(self):
return str(self.obj)
def __getstate__(self):
return self.obj
def _also(self, value):
return self.obj == value
class NotEqualTo(Also):
"""Matches any object that is not equal to the specified object."""
def __init__(self, pattern, obj):
super().__init__(pattern)
self.obj = obj
def __repr__(self):
return f"<!={self.obj!r}>"
def _also(self, value):
return self.obj != value
class SameAs(Also):
"""Matches one specific object only (i.e. makes '==' behave like 'is')."""
def __init__(self, pattern, obj):
super().__init__(pattern)
self.obj = obj
def __repr__(self):
return f"<is {self.obj!r}>"
def _also(self, value):
return self.obj is value
class Matching(Also):
"""Matches any string that matches the specified regular expression."""
def __init__(self, pattern, regex, flags=0):
assert isinstance(regex, bytes) or isinstance(regex, str)
super().__init__(pattern)
self.regex = regex
self.flags = flags
def __repr__(self):
s = repr(self.regex)
if s[0] in "bu":
return s[0] + "/" + s[2:-1] + "/"
else:
return "/" + s[1:-1] + "/"
def _also(self, value):
regex = self.regex
# re.match() always starts matching at the beginning, but does not require
# a complete match of the string - append "$" to ensure the latter.
if isinstance(regex, bytes):
if not isinstance(value, bytes):
return NotImplemented
regex += b"$"
elif isinstance(regex, str):
if not isinstance(value, str):
return NotImplemented
regex += "$"
else:
raise AssertionError()
return re.match(regex, value, self.flags) is not None