util.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Utility."""
  2. from functools import wraps, lru_cache
  3. import warnings
  4. import re
  5. from typing import Callable, Any, Optional, Tuple, List
  6. DEBUG = 0x00001
  7. RE_PATTERN_LINE_SPLIT = re.compile(r'(?:\r\n|(?!\r\n)[\n\r])|$')
  8. UC_A = ord('A')
  9. UC_Z = ord('Z')
  10. @lru_cache(maxsize=512)
  11. def lower(string: str) -> str:
  12. """Lower."""
  13. new_string = []
  14. for c in string:
  15. o = ord(c)
  16. new_string.append(chr(o + 32) if UC_A <= o <= UC_Z else c)
  17. return ''.join(new_string)
  18. class SelectorSyntaxError(Exception):
  19. """Syntax error in a CSS selector."""
  20. def __init__(self, msg: str, pattern: Optional[str] = None, index: Optional[int] = None) -> None:
  21. """Initialize."""
  22. self.line = None
  23. self.col = None
  24. self.context = None
  25. if pattern is not None and index is not None:
  26. # Format pattern to show line and column position
  27. self.context, self.line, self.col = get_pattern_context(pattern, index)
  28. msg = '{}\n line {}:\n{}'.format(msg, self.line, self.context)
  29. super().__init__(msg)
  30. def deprecated(message: str, stacklevel: int = 2) -> Callable[..., Any]: # pragma: no cover
  31. """
  32. Raise a `DeprecationWarning` when wrapped function/method is called.
  33. Usage:
  34. @deprecated("This method will be removed in version X; use Y instead.")
  35. def some_method()"
  36. pass
  37. """
  38. def _wrapper(func: Callable[..., Any]) -> Callable[..., Any]:
  39. @wraps(func)
  40. def _deprecated_func(*args: Any, **kwargs: Any) -> Any:
  41. warnings.warn(
  42. f"'{func.__name__}' is deprecated. {message}",
  43. category=DeprecationWarning,
  44. stacklevel=stacklevel
  45. )
  46. return func(*args, **kwargs)
  47. return _deprecated_func
  48. return _wrapper
  49. def warn_deprecated(message: str, stacklevel: int = 2) -> None: # pragma: no cover
  50. """Warn deprecated."""
  51. warnings.warn(
  52. message,
  53. category=DeprecationWarning,
  54. stacklevel=stacklevel
  55. )
  56. def get_pattern_context(pattern: str, index: int) -> Tuple[str, int, int]:
  57. """Get the pattern context."""
  58. last = 0
  59. current_line = 1
  60. col = 1
  61. text = [] # type: List[str]
  62. line = 1
  63. offset = None # type: Optional[int]
  64. # Split pattern by newline and handle the text before the newline
  65. for m in RE_PATTERN_LINE_SPLIT.finditer(pattern):
  66. linetext = pattern[last:m.start(0)]
  67. if not len(m.group(0)) and not len(text):
  68. indent = ''
  69. offset = -1
  70. col = index - last + 1
  71. elif last <= index < m.end(0):
  72. indent = '--> '
  73. offset = (-1 if index > m.start(0) else 0) + 3
  74. col = index - last + 1
  75. else:
  76. indent = ' '
  77. offset = None
  78. if len(text):
  79. # Regardless of whether we are presented with `\r\n`, `\r`, or `\n`,
  80. # we will render the output with just `\n`. We will still log the column
  81. # correctly though.
  82. text.append('\n')
  83. text.append('{}{}'.format(indent, linetext))
  84. if offset is not None:
  85. text.append('\n')
  86. text.append(' ' * (col + offset) + '^')
  87. line = current_line
  88. current_line += 1
  89. last = m.end(0)
  90. return ''.join(text), line, col