Tools used by OpenStack Documentation
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.
 
 
 
 

163 lines
5.0 KiB

  1. #!/usr/bin/env python
  2. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  3. # not use this file except in compliance with the License. You may obtain
  4. # a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software
  9. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  10. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  11. # License for the specific language governing permissions and limitations
  12. # under the License.
  13. """
  14. Usage:
  15. jsoncheck.py [-f] FILES
  16. Checks JSON syntax and optionally reformats (pretty-prints) the valid files.
  17. Optional:
  18. - demjson Python library (better diagnostics for invalid JSON synax)
  19. """
  20. from __future__ import print_function
  21. import argparse
  22. import collections
  23. import json
  24. import sys
  25. import textwrap
  26. try:
  27. import demjson
  28. except ImportError:
  29. demjson = None
  30. sys.stderr.write("Cannot import the demjson Python module. Diagnostics "
  31. "for invalid JSON files\nwill be limited.\n")
  32. # -----------------------------------------------------------------------------
  33. # Public interface
  34. # -----------------------------------------------------------------------------
  35. def check_syntax(path):
  36. """Check syntax of one JSON file."""
  37. _process_file(path)
  38. def check_formatting(path):
  39. """Check formatting of one JSON file."""
  40. _process_file(path, formatting='check')
  41. def fix_formatting(path, verbose=False):
  42. """Fix formatting of one JSON file."""
  43. _process_file(path, formatting='fix', verbose=verbose)
  44. # -----------------------------------------------------------------------------
  45. # Implementation details
  46. # -----------------------------------------------------------------------------
  47. def _indent_note(note):
  48. """Indents and wraps a string."""
  49. indented_note = []
  50. # Split into single lines in case the argument is pre-formatted.
  51. for line in note.splitlines():
  52. indented_note.append(textwrap.fill(line, initial_indent=4 * ' ',
  53. subsequent_indent=12 * ' ',
  54. width=80))
  55. return "\n".join(indented_note)
  56. def _get_demjson_diagnostics(raw):
  57. """Get diagnostics string for invalid JSON files from demjson."""
  58. errstr = None
  59. try:
  60. demjson.decode(raw, strict=True)
  61. except demjson.JSONError as err:
  62. errstr = err.pretty_description()
  63. return errstr
  64. class ParserException(Exception):
  65. pass
  66. def _parse_json(raw):
  67. """Parse raw JSON file."""
  68. try:
  69. parsed = json.loads(raw, object_pairs_hook=collections.OrderedDict)
  70. except ValueError as err:
  71. note = str(err)
  72. # if demjson is available, print its diagnostic string as well
  73. if demjson:
  74. demerr = _get_demjson_diagnostics(raw)
  75. if demerr:
  76. note += "\n" + demerr
  77. raise ParserException(note)
  78. else:
  79. return parsed
  80. def _format_parsed_json(parsed):
  81. """Pretty-print JSON file content while retaining key order."""
  82. return json.dumps(parsed, sort_keys=False, separators=(',', ': '),
  83. indent=4) + "\n"
  84. def _process_file(path, formatting=None, verbose=False):
  85. """Check syntax/formatting and fix formatting of a JSON file.
  86. :param formatting: one of 'check' or 'fix' (default: None)
  87. Raises ValueError if JSON syntax is invalid or reformatting needed.
  88. """
  89. with open(path, 'r') as infile:
  90. raw = infile.read()
  91. try:
  92. parsed = _parse_json(raw)
  93. except ParserException as err:
  94. raise ValueError(err)
  95. else:
  96. if formatting in ('check', 'fix'):
  97. formatted = _format_parsed_json(parsed)
  98. if formatted != raw:
  99. if formatting == "fix":
  100. with open(path, 'w') as outfile:
  101. outfile.write(formatted)
  102. if verbose:
  103. print("%s\n%s" % (path,
  104. _indent_note("Reformatted")))
  105. else:
  106. raise ValueError("Reformatting needed")
  107. elif formatting is not None:
  108. # for the benefit of external callers
  109. raise ValueError("Called with invalid formatting value.")
  110. def main():
  111. parser = argparse.ArgumentParser(description="Validate and reformat JSON"
  112. "files.")
  113. parser.add_argument('files', metavar='FILES', nargs='+')
  114. parser.add_argument('-f', '--formatting', choices=['check', 'fix'],
  115. help='check or fix formatting of JSON files')
  116. args = parser.parse_args()
  117. exit_status = 0
  118. for path in args.files:
  119. try:
  120. _process_file(path, args.formatting, verbose=True)
  121. except ValueError as err:
  122. print("%s\n%s" % (path, _indent_note(str(err))))
  123. exit_status = 1
  124. return exit_status
  125. if __name__ == "__main__":
  126. sys.exit(main())