Tools to make Jenkins jobs from templates
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

formatter.py 5.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. #!/usr/bin/env python
  2. # Copyright (C) 2015 OpenStack, LLC.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. # Manage interpolation of JJB variables into template strings.
  16. import logging
  17. from pprint import pformat
  18. import re
  19. from string import Formatter
  20. from jenkins_jobs.errors import JenkinsJobsException
  21. from jenkins_jobs.local_yaml import CustomLoader
  22. logger = logging.getLogger(__name__)
  23. def deep_format(obj, paramdict, allow_empty=False):
  24. """Apply the paramdict via str.format() to all string objects found within
  25. the supplied obj. Lists and dicts are traversed recursively."""
  26. # YAML serialisation was originally used to achieve this, but that places
  27. # limitations on the values in paramdict - the post-format result must
  28. # still be valid YAML (so substituting-in a string containing quotes, for
  29. # example, is problematic).
  30. if hasattr(obj, 'format'):
  31. try:
  32. ret = CustomFormatter(allow_empty).format(obj, **paramdict)
  33. except KeyError as exc:
  34. missing_key = exc.args[0]
  35. desc = "%s parameter missing to format %s\nGiven:\n%s" % (
  36. missing_key, obj, pformat(paramdict))
  37. raise JenkinsJobsException(desc)
  38. except Exception:
  39. logging.error("Problem formatting with args:\nallow_empty:"
  40. "%s\nobj: %s\nparamdict: %s" %
  41. (allow_empty, obj, paramdict))
  42. raise
  43. elif isinstance(obj, list):
  44. ret = type(obj)()
  45. for item in obj:
  46. ret.append(deep_format(item, paramdict, allow_empty))
  47. elif isinstance(obj, dict):
  48. ret = type(obj)()
  49. for item in obj:
  50. try:
  51. ret[CustomFormatter(allow_empty).format(item, **paramdict)] = \
  52. deep_format(obj[item], paramdict, allow_empty)
  53. except KeyError as exc:
  54. missing_key = exc.args[0]
  55. desc = "%s parameter missing to format %s\nGiven:\n%s" % (
  56. missing_key, obj, pformat(paramdict))
  57. raise JenkinsJobsException(desc)
  58. except Exception:
  59. logging.error("Problem formatting with args:\nallow_empty:"
  60. "%s\nobj: %s\nparamdict: %s" %
  61. (allow_empty, obj, paramdict))
  62. raise
  63. else:
  64. ret = obj
  65. if isinstance(ret, CustomLoader):
  66. # If we have a CustomLoader here, we've lazily-loaded a template;
  67. # attempt to format it.
  68. ret = deep_format(ret, paramdict, allow_empty=allow_empty)
  69. return ret
  70. class CustomFormatter(Formatter):
  71. """
  72. Custom formatter to allow non-existing key references when formatting a
  73. string
  74. """
  75. _expr = """
  76. (?<!{){({{)* # non-pair opening {
  77. (?:obj:)? # obj:
  78. (?P<key>\w+) # key
  79. (?:\|(?P<default>[^}]*))? # default fallback
  80. }(}})*(?!}) # non-pair closing }
  81. """
  82. def __init__(self, allow_empty=False):
  83. super(CustomFormatter, self).__init__()
  84. self.allow_empty = allow_empty
  85. def vformat(self, format_string, args, kwargs):
  86. matcher = re.compile(self._expr, re.VERBOSE)
  87. # special case of returning the object if the entire string
  88. # matches a single parameter
  89. try:
  90. result = re.match('^%s$' % self._expr, format_string, re.VERBOSE)
  91. except TypeError:
  92. return format_string.format(**kwargs)
  93. if result is not None:
  94. try:
  95. return kwargs[result.group("key")]
  96. except KeyError:
  97. pass
  98. # handle multiple fields within string via a callback to re.sub()
  99. def re_replace(match):
  100. key = match.group("key")
  101. default = match.group("default")
  102. if default is not None:
  103. if key not in kwargs:
  104. return default
  105. else:
  106. return "{%s}" % key
  107. return match.group(0)
  108. format_string = matcher.sub(re_replace, format_string)
  109. return Formatter.vformat(self, format_string, args, kwargs)
  110. def get_value(self, key, args, kwargs):
  111. try:
  112. return Formatter.get_value(self, key, args, kwargs)
  113. except KeyError:
  114. if self.allow_empty:
  115. logger.debug(
  116. 'Found uninitialized key %s, replaced with empty string',
  117. key
  118. )
  119. return ''
  120. raise