docs: def replace_macros(string, spec=None):
      """Replace all macros in given string with corresponding values.
  
      For example: a string '%{name}-%{version}.tar.gz' will be transformed to 'foo-2.0.tar.gz'.
  
      :param string A string containing macros that you want to be replaced
      :param spec An optional spec file. If given, definitions in that spec
      file will be used to replace macros.
  
      :return A string where all macros in given input are substituted as good as possible.
  
      """
      if spec:
          assert isinstance(spec, Spec)
  
      def _is_conditional(macro: str) -> bool:
          return macro.startswith("?") or macro.startswith("!")
  
      def _test_conditional(macro: str) -> bool:
          if macro[0] == "?":
              return True
          if macro[0] == "!":
              return False
          raise Exception("Given string is not a conditional macro")
  
      def _macro_repl(match):
          macro_name = match.group(1)
          if _is_conditional(macro_name) and spec:
              parts = macro_name[1:].split(sep=":", maxsplit=1)
              assert parts
              if _test_conditional(macro_name):  # ?
                  if hasattr(spec, parts[0]):
                      if len(parts) == 2:
                          return parts[1]
  
                      return getattr(spec, parts[0], None)
  
                  return ""
              else:  # !
                  if not hasattr(spec, parts[0]):
                      if len(parts) == 2:
                          return parts[1]
  
                      return getattr(spec, parts[0], None)
  
                  return ""
  
          if spec:
              value = getattr(spec, macro_name, None)
              if value:
                  return str(value)
          return match.string[match.start() : match.end()]
  
      # Recursively expand macros
      # Note: If macros are not defined in the spec file, this won't try to
      # expand them.
      while True:
          ret = re.sub(_macro_pattern, _macro_repl, string)
          if ret != string:
              string = ret
              continue
          return ret