Skip to content

Commit 61444b3

Browse files
committed
added option --stdin-json-header
1 parent aeefee8 commit 61444b3

4 files changed

Lines changed: 87 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33

44
## v0.9.0 (dev)
55

6+
* Added option `--stdin-json-header`, which treats the first line of stdin as
7+
a header in JSON format containing data about stdin such as file origin.
8+
This allows Codebraid Preview to transmit multiple, concatenated files via
9+
stdin while still getting correct file names and line numbers in error and
10+
warning messages, rather than a file name like `<string>` and a number based
11+
on concatenated file length. It also allows Codebraid Preview and normal
12+
Codebraid processing to share the same cache, since Codebraid can associate
13+
the cache with specific file paths rather than just generic stdin.
14+
615
* Fixed a caching bug. Errors due to missing executables (built-in code
716
execution system), missing Jupyter kernels, ambiguous Jupyter kernel names,
817
and some Jupyter configuration issues were only reported during an initial

codebraid/cmdline.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
import argparse
1515
import io
16+
import json
1617
import pathlib
1718
import sys
1819
from . import converters
20+
from . import util
1921
from .version import __version__ as version
2022

2123

@@ -88,6 +90,8 @@ def print_help(self):
8890
'Output from execution is written as soon as it becomes available. '
8991
'No document is created. '
9092
'Options --to and --output are only used (if at all) to inform code output formatting.')
93+
parser_pandoc.add_argument('--stdin-json-header', action='store_true',
94+
help='Treat the first line of stdin as a header in JSON format containing data about stdin such as file origin.')
9195
parser_pandoc.add_argument('files', nargs='*', metavar='FILE',
9296
help="Files (multiple files are allowed for formats supported by Pandoc)")
9397
for opts_or_long_opt, narg in PANDOC_OPTIONS.items():
@@ -154,13 +158,43 @@ def pandoc(args):
154158

155159
if not args.files or (len(args.files) == 1 and args.files[0] == '-'):
156160
paths = None
161+
string_origins = None
157162
try:
158163
strings = sys.stdin.read()
159164
except UnicodeDecodeError as e:
160-
sys.exit('Input must be UTF-8:\n{0}'.format(e))
165+
sys.exit(f'Input must be UTF-8:\n{e}')
166+
if args.stdin_json_header:
167+
stdin_lines = util.splitlines_lf(strings)
168+
if not stdin_lines:
169+
sys.exit('Missing data for --stdin-json-header')
170+
if len(stdin_lines) == 1:
171+
stdin_lines.append('')
172+
json_header_str = stdin_lines[0]
173+
try:
174+
json_header = json.loads(json_header_str)
175+
except Exception as e:
176+
sys.exit(f'Invalid data for --stdin-json-header:\n{e}')
177+
if not isinstance(json_header, dict):
178+
sys.exit(f'Invalid data for --stdin-json-header: expected dict')
179+
json_header_origins = json_header.get('origins')
180+
if json_header_origins is not None:
181+
if not isinstance(json_header_origins, list):
182+
sys.exit('Invalid data for --stdin-json-header field "string_origins"')
183+
start_line = 1
184+
strings = []
185+
string_origins = []
186+
for entry in json_header_origins:
187+
origin_name = entry['path']
188+
origin_length = entry['lines']
189+
string_origins.append(origin_name)
190+
strings.append('\n'.join(stdin_lines[start_line:start_line+origin_length]))
191+
start_line += origin_length
192+
else:
193+
strings = '\n'.join(stdin_lines[1:])
161194
else:
162195
paths = args.files
163196
strings = None
197+
string_origins = None
164198

165199
code_defaults = {}
166200
session_defaults = {}
@@ -187,6 +221,7 @@ def pandoc(args):
187221
with converters.PandocConverter(
188222
paths=paths,
189223
strings=strings,
224+
string_origins=string_origins,
190225
from_format=args.from_format,
191226
pandoc_file_scope=args.pandoc_file_scope,
192227
no_cache=args.no_cache,

codebraid/converters/base.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class Converter(object):
5858

5959
def __init__(self, *,
6060
strings: Optional[Union[str, Sequence[str]]]=None,
61+
string_origins: Optional[Union[str, pathlib.Path, Sequence[str], Sequence[pathlib.Path]]]=None,
6162
paths: Optional[Union[str, Sequence[str], pathlib.Path, Sequence[pathlib.Path]]]=None,
6263
no_cache: Optional[bool]=False,
6364
cache_path: Optional[Union[str, pathlib.Path]]=None,
@@ -80,6 +81,8 @@ def __init__(self, *,
8081
self.session_defaults = session_defaults
8182

8283
if paths is not None and strings is None:
84+
if string_origins is not None:
85+
raise TypeError
8386
if isinstance(paths, str):
8487
paths = [pathlib.Path(paths)]
8588
elif isinstance(paths, pathlib.Path):
@@ -135,15 +138,47 @@ def __init__(self, *,
135138
elif not (isinstance(strings, collections.abc.Sequence) and
136139
strings and all(isinstance(x, str) for x in strings)):
137140
raise TypeError
141+
string_origins_normalized: Optional[Union[Sequence[str], Sequence[pathlib.Path]]]
142+
if string_origins is None:
143+
string_origins_normalized = None
144+
elif isinstance(string_origins, str) or isinstance(string_origins, pathlib.Path):
145+
string_origins_normalized = [string_origins]
146+
elif (isinstance(string_origins, collections.abc.Sequence) and string_origins and
147+
(all(isinstance(x, str) for x in string_origins) or all(isinstance(x, pathlib.Path) for x in string_origins))):
148+
string_origins_normalized = string_origins
149+
else:
150+
raise TypeError
151+
138152
# Normalize newlines, as if read from file with universal newlines
139-
origin_strings = [io.StringIO(s, newline=None).read() or '\n' for s in strings]
140-
if len(strings) == 1:
141-
origin_names = ['<string>']
153+
origin_strings = []
154+
for s in strings:
155+
if '\r' in s:
156+
origin_strings.append(io.StringIO(s, newline=None).read() or '\n')
157+
else:
158+
origin_strings.append(s or '\n')
159+
if string_origins_normalized is not None:
160+
origin_names = []
161+
self.raw_origin_paths = []
162+
self.expanded_origin_paths = {}
163+
cwd_path = pathlib.Path('.').absolute()
164+
for string_origin in string_origins_normalized:
165+
string_origin_path = pathlib.Path(string_origin)
166+
try:
167+
string_origin_rel_path = string_origin_path.relative_to(cwd_path)
168+
except ValueError:
169+
string_origin_rel_path = string_origin_path
170+
origin_name = string_origin_rel_path.as_posix()
171+
origin_names.append(origin_name)
172+
self.raw_origin_paths.append(string_origin_path)
173+
self.expanded_origin_paths[origin_name] = string_origin_path
142174
else:
143-
origin_names = ['<string({0})>'.format(n+1) for n in range(len(strings))]
175+
if len(strings) == 1:
176+
origin_names = ['<string>']
177+
else:
178+
origin_names = ['<string({0})>'.format(n+1) for n in range(len(strings))]
179+
self.raw_origin_paths = None
180+
self.expanded_origin_paths = None
144181
self.origins = collections.OrderedDict(zip(origin_names, origin_strings))
145-
self.raw_origin_paths = None
146-
self.expanded_origin_paths = None
147182
if from_format is None:
148183
raise TypeError('Document format is required')
149184
if self.from_formats is not None and from_format not in self.from_formats:

codebraid/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# -*- coding: utf-8 -*-
22

33
from .fmtversion import get_version_plus_info
4-
__version__, __version_info__ = get_version_plus_info(0, 9, 0, 'dev', 5)
4+
__version__, __version_info__ = get_version_plus_info(0, 9, 0, 'dev', 6)

0 commit comments

Comments
 (0)