Tighter checks for figures
[rnaseq-cwl-training.git] / bin / util.py
1 import sys
2 import os
3 import json
4 from subprocess import Popen, PIPE
5
6 # Import this way to produce a more useful error message.
7 try:
8     import yaml
9 except ImportError:
10     print('Unable to import YAML module: please install PyYAML', file=sys.stderr)
11     sys.exit(1)
12
13
14 # Things an image file's name can end with.
15 IMAGE_FILE_SUFFIX = {
16     '.gif',
17     '.jpg',
18     '.png',
19     '.svg'
20 }
21
22 # Files that shouldn't be present.
23 UNWANTED_FILES = [
24     '.nojekyll'
25 ]
26
27 # Marker to show that an expected value hasn't been provided.
28 # (Can't use 'None' because that might be a legitimate value.)
29 REPORTER_NOT_SET = []
30
31 class Reporter(object):
32     """Collect and report errors."""
33
34     def __init__(self):
35         """Constructor."""
36
37         super(Reporter, self).__init__()
38         self.messages = []
39
40
41     def check_field(self, filename, name, values, key, expected=REPORTER_NOT_SET):
42         """Check that a dictionary has an expected value."""
43
44         if key not in values:
45             self.add(filename, '{0} does not contain {1}', name, key)
46         elif expected is REPORTER_NOT_SET:
47             pass
48         elif type(expected) in (tuple, set, list):
49             if values[key] not in expected:
50                 self.add(filename, '{0} {1} value {2} is not in {3}', name, key, values[key], expected)
51         elif values[key] != expected:
52             self.add(filename, '{0} {1} is {2} not {3}', name, key, values[key], expected)
53
54
55     def check(self, condition, location, fmt, *args):
56         """Append error if condition not met."""
57
58         if not condition:
59             self.add(location, fmt, *args)
60
61
62     def add(self, location, fmt, *args):
63         """Append error unilaterally."""
64
65         if isinstance(location, type(None)):
66             coords = ''
67         elif isinstance(location, str):
68             coords = '{0}: '.format(location)
69         elif isinstance(location, tuple):
70             filename, line_number = location
71             coords = '{0}:{1}: '.format(*location)
72         else:
73             assert False, 'Unknown location "{0}"/{1}'.format(location, type(location))
74
75         self.messages.append(coords + fmt.format(*args))
76
77
78     def report(self, stream=sys.stdout):
79         """Report all messages."""
80
81         if not self.messages:
82             return
83         for m in sorted(self.messages):
84             print(m, file=stream)
85
86
87 def read_markdown(parser, path):
88     """
89     Get YAML and AST for Markdown file, returning
90     {'metadata':yaml, 'metadata_len':N, 'text':text, 'lines':[(i, line, len)], 'doc':doc}.
91     """
92
93     # Split and extract YAML (if present).
94     with open(path, 'r') as reader:
95         body = reader.read()
96     metadata_raw, metadata_yaml, body = split_metadata(path, body)
97
98     # Split into lines.
99     metadata_len = 0 if metadata_raw is None else metadata_raw.count('\n')
100     lines = [(metadata_len+i+1, line, len(line)) for (i, line) in enumerate(body.split('\n'))]
101
102     # Parse Markdown.
103     cmd = 'ruby {0}'.format(parser)
104     p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, universal_newlines=True)
105     stdout_data, stderr_data = p.communicate(body)
106     doc = json.loads(stdout_data)
107
108     return {
109         'metadata': metadata_yaml,
110         'metadata_len': metadata_len,
111         'text': body,
112         'lines': lines,
113         'doc': doc
114     }
115
116
117 def split_metadata(path, text):
118     """
119     Get raw (text) metadata, metadata as YAML, and rest of body.
120     If no metadata, return (None, None, body).
121     """
122
123     metadata_raw = None
124     metadata_yaml = None
125     metadata_len = None
126
127     pieces = text.split('---', 2)
128     if len(pieces) == 3:
129         metadata_raw = pieces[1]
130         text = pieces[2]
131         try:
132             metadata_yaml = yaml.load(metadata_raw)
133         except yaml.YAMLError as e:
134             print('Unable to parse YAML header in {0}:\n{1}'.format(path, e), file=sys.stderr)
135             sys.exit(1)
136
137     return metadata_raw, metadata_yaml, text
138
139
140 def load_yaml(filename):
141     """
142     Wrapper around YAML loading so that 'import yaml' is only needed
143     in one file.
144     """
145
146     try:
147         with open(filename, 'r') as reader:
148             return yaml.load(reader)
149     except (yaml.YAMLError, FileNotFoundError) as e:
150         print('Unable to load YAML file {0}:\n{1}'.format(filename, e), file=sys.stderr)
151         sys.exit(1)
152
153
154 def check_unwanted_files(dir_path, reporter):
155     """
156     Check that unwanted files are not present.
157     """
158
159     for filename in UNWANTED_FILES:
160         path = os.path.join(dir_path, filename)
161         reporter.check(not os.path.exists(path),
162                        path,
163                        "Unwanted file found")
164
165
166 def require(condition, message):
167     """Fail if condition not met."""
168
169     if not condition:
170         print(message, file=sys.stderr)
171         sys.exit(1)