code_template.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import re
  2. from typing import Match, Optional, Sequence, Mapping
  3. # match $identifier or ${identifier} and replace with value in env
  4. # If this identifier is at the beginning of whitespace on a line
  5. # and its value is a list then it is treated as
  6. # block substitution by indenting to that depth and putting each element
  7. # of the list on its own line
  8. # if the identifier is on a line starting with non-whitespace and a list
  9. # then it is comma separated ${,foo} will insert a comma before the list
  10. # if this list is not empty and ${foo,} will insert one after.
  11. class CodeTemplate:
  12. # Python 2.7.5 has a bug where the leading (^[^\n\S]*)? does not work,
  13. # workaround via appending another [^\n\S]? inside
  14. substitution_str = r"(^[^\n\S]*[^\n\S]?)?\$([^\d\W]\w*|\{,?[^\d\W]\w*\,?})"
  15. # older versions of Python have a bug where \w* does not work,
  16. # so we need to replace with the non-shortened version [a-zA-Z0-9_]*
  17. # https://bugs.python.org/issue18647
  18. substitution_str = substitution_str.replace(r"\w", r"[a-zA-Z0-9_]")
  19. substitution = re.compile(substitution_str, re.MULTILINE)
  20. pattern: str
  21. filename: str
  22. @staticmethod
  23. def from_file(filename: str) -> "CodeTemplate":
  24. with open(filename, "r") as f:
  25. return CodeTemplate(f.read(), filename)
  26. def __init__(self, pattern: str, filename: str = "") -> None:
  27. self.pattern = pattern
  28. self.filename = filename
  29. def substitute(
  30. self, env: Optional[Mapping[str, object]] = None, **kwargs: object
  31. ) -> str:
  32. if env is None:
  33. env = {}
  34. def lookup(v: str) -> object:
  35. assert env is not None
  36. return kwargs[v] if v in kwargs else env[v]
  37. def indent_lines(indent: str, v: Sequence[object]) -> str:
  38. return "".join(
  39. [indent + l + "\n" for e in v for l in str(e).splitlines()]
  40. ).rstrip()
  41. def replace(match: Match[str]) -> str:
  42. indent = match.group(1)
  43. key = match.group(2)
  44. comma_before = ""
  45. comma_after = ""
  46. if key[0] == "{":
  47. key = key[1:-1]
  48. if key[0] == ",":
  49. comma_before = ", "
  50. key = key[1:]
  51. if key[-1] == ",":
  52. comma_after = ", "
  53. key = key[:-1]
  54. v = lookup(key)
  55. if indent is not None:
  56. if not isinstance(v, list):
  57. v = [v]
  58. return indent_lines(indent, v)
  59. elif isinstance(v, list):
  60. middle = ", ".join([str(x) for x in v])
  61. if len(v) == 0:
  62. return middle
  63. return comma_before + middle + comma_after
  64. else:
  65. return str(v)
  66. return self.substitution.sub(replace, self.pattern)
  67. if __name__ == "__main__":
  68. c = CodeTemplate(
  69. """\
  70. int foo($args) {
  71. $bar
  72. $bar
  73. $a+$b
  74. }
  75. int commatest(int a${,stuff})
  76. int notest(int a${,empty,})
  77. """
  78. )
  79. print(
  80. c.substitute(
  81. args=["hi", 8],
  82. bar=["what", 7],
  83. a=3,
  84. b=4,
  85. stuff=["things...", "others"],
  86. empty=[],
  87. )
  88. )