mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
387 lines
10 KiB
Python
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
|