Skip to content

Commit 1d3f976

Browse files
gjchong25Grace Chong
andauthored
DOP-2952: Literalinclude support for input/output directives (#398)
* DOP-2952: Literalinclude support for input directives * add tests for full literalinclude functionality support * add descriptors to rstspec.toml Co-authored-by: Grace Chong <[email protected]>
1 parent b6c777d commit 1d3f976

File tree

3 files changed

+126
-70
lines changed

3 files changed

+126
-70
lines changed

snooty/parser.py

Lines changed: 64 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -727,85 +727,79 @@ def create_page() -> Tuple[Page, EmbeddedRstParser]:
727727
# Capture the original file-length before splicing it
728728
len_file = len(lines)
729729

730-
if name == "literalinclude":
731-
732-
def _locate_text(text: str) -> int:
733-
"""
734-
Searches the literally-included file ('lines') for the specified text. If no such text is found,
735-
add an InvalidLiteralInclude diagnostic.
736-
"""
737-
assert isinstance(text, str)
738-
loc = next(
739-
(idx for idx, line in enumerate(lines) if text in line), -1
730+
def _locate_text(text: str) -> int:
731+
"""
732+
Searches the literally-included file ('lines') for the specified text. If no such text is found,
733+
add an InvalidLiteralInclude diagnostic.
734+
"""
735+
assert isinstance(text, str)
736+
loc = next((idx for idx, line in enumerate(lines) if text in line), -1)
737+
if loc < 0:
738+
self.diagnostics.append(
739+
InvalidLiteralInclude(f'"{text}" not found in {filepath}', line)
740740
)
741-
if loc < 0:
742-
self.diagnostics.append(
743-
InvalidLiteralInclude(
744-
f'"{text}" not found in {filepath}', line
745-
)
746-
)
747-
return loc
748-
749-
# Locate the start_after query
750-
start_after = 0
751-
if "start-after" in options:
752-
start_after_text = options["start-after"]
753-
# start_after = self._locate_text(start_after_text, lines, line, text)
754-
start_after = _locate_text(start_after_text)
755-
# Only increment start_after if text is specified, to avoid capturing the start_after_text
756-
start_after += 1
757-
758-
# ...now locate the end_before query
741+
return loc
742+
743+
# Locate the start_after query
744+
start_after = 0
745+
if "start-after" in options:
746+
start_after_text = options["start-after"]
747+
# start_after = self._locate_text(start_after_text, lines, line, text)
748+
start_after = _locate_text(start_after_text)
749+
# Only increment start_after if text is specified, to avoid capturing the start_after_text
750+
start_after += 1
751+
752+
# ...now locate the end_before query
753+
end_before = len(lines)
754+
if "end-before" in options:
755+
end_before_text = options["end-before"]
756+
# end_before = self._locate_text(end_before_text, lines, line, text)
757+
end_before = _locate_text(end_before_text)
758+
759+
# Check that start_after_text precedes end_before_text (and end_before exists)
760+
if start_after >= end_before >= 0:
761+
self.diagnostics.append(
762+
InvalidLiteralInclude(
763+
f'"{end_before_text}" precedes "{start_after_text}" in {filepath}',
764+
line,
765+
)
766+
)
767+
768+
# If we failed to locate end_before text, default to the end-of-file
769+
if end_before == -1:
759770
end_before = len(lines)
760-
if "end-before" in options:
761-
end_before_text = options["end-before"]
762-
# end_before = self._locate_text(end_before_text, lines, line, text)
763-
end_before = _locate_text(end_before_text)
764771

765-
# Check that start_after_text precedes end_before_text (and end_before exists)
766-
if start_after >= end_before >= 0:
772+
lines = lines[start_after:end_before]
773+
774+
dedent = 0
775+
if "dedent" in options:
776+
# Dedent is specified as a flag
777+
if isinstance(options["dedent"], bool):
778+
# Deduce a reasonable dedent
779+
try:
780+
dedent = min(
781+
len(line) - len(line.lstrip())
782+
for line in lines
783+
if len(line.lstrip()) > 0
784+
)
785+
except ValueError:
786+
# Handle the (unlikely) case where there are no non-empty lines
787+
dedent = 0
788+
# Dedent is specified as a nonnegative integer (number of characters):
789+
# Note: since boolean is a subtype of int, this conditonal must follow the
790+
# above bool-type conditional.
791+
elif isinstance(options["dedent"], int):
792+
dedent = options["dedent"]
793+
else:
767794
self.diagnostics.append(
768795
InvalidLiteralInclude(
769-
f'"{end_before_text}" precedes "{start_after_text}" in {filepath}',
796+
f'Dedent "{dedent}" of type {type(dedent)}; expected nonnegative integer or flag',
770797
line,
771798
)
772799
)
800+
return doc
773801

774-
# If we failed to locate end_before text, default to the end-of-file
775-
if end_before == -1:
776-
end_before = len(lines)
777-
778-
lines = lines[start_after:end_before]
779-
780-
dedent = 0
781-
if "dedent" in options:
782-
# Dedent is specified as a flag
783-
if isinstance(options["dedent"], bool):
784-
# Deduce a reasonable dedent
785-
try:
786-
dedent = min(
787-
len(line) - len(line.lstrip())
788-
for line in lines
789-
if len(line.lstrip()) > 0
790-
)
791-
except ValueError:
792-
# Handle the (unlikely) case where there are no non-empty lines
793-
dedent = 0
794-
# Dedent is specified as a nonnegative integer (number of characters):
795-
# Note: since boolean is a subtype of int, this conditonal must follow the
796-
# above bool-type conditional.
797-
elif isinstance(options["dedent"], int):
798-
dedent = options["dedent"]
799-
else:
800-
self.diagnostics.append(
801-
InvalidLiteralInclude(
802-
f'Dedent "{dedent}" of type {type(dedent)}; expected nonnegative integer or flag',
803-
line,
804-
)
805-
)
806-
return doc
807-
808-
lines = [line[dedent:] for line in lines]
802+
lines = [line[dedent:] for line in lines]
809803

810804
emphasize_lines = None
811805
if "emphasize-lines" in options:

snooty/rstspec.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,10 +499,18 @@ help = """The code content."""
499499
argument_type = ["path", "raw"]
500500
content_type = "raw"
501501
options.language = "string"
502+
options.start-after = "string"
503+
options.end-before = "string"
504+
options.dedent = ["nonnegative_integer", "flag"]
505+
options.lineno-start = "nonnegative_integer"
502506
options.emphasize-lines = "linenos"
503507
options.linenos = "flag"
504508
example = """.. input:: ${0:code input or </path/to/file>}
505509
:language: ${1:language}
510+
:start-after: ${2:text to start after (Optional}}
511+
:end-before: ${3:text to end before (Optional}}
512+
:dedent: ${4:number of chars to dedent by}
513+
:lineno-start: ${5:nonnegative integer to start line numbering from}
506514
:emphasize-lines: (string)
507515
:linenos: (flag)
508516
"""
@@ -512,11 +520,19 @@ help = """The code output."""
512520
argument_type = ["path", "raw"]
513521
content_type = "raw"
514522
options.language = "string"
523+
options.start-after = "string"
524+
options.end-before = "string"
525+
options.dedent = ["nonnegative_integer", "flag"]
526+
options.lineno-start = "nonnegative_integer"
515527
options.emphasize-lines = "linenos"
516528
options.linenos = "flag"
517529
options.visible = "boolean"
518530
example = """.. output:: ${0:code output or </path/to/file>}
519531
:language: ${1:language}
532+
:start-after: ${2:text to start after (Optional}}
533+
:end-before: ${3:text to end before (Optional}}
534+
:dedent: ${4:number of chars to dedent by}
535+
:lineno-start: ${5:nonnegative integer to start line numbering from}
520536
:emphasize-lines: (string)
521537
:linenos: (flag)
522538
:visible: (bool)

snooty/test_parser.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,52 @@ def test_iocodeblock() -> None:
654654
</root>""",
655655
)
656656

657+
# Test full support of literalinclude options for input/output directives
658+
page, diagnostics = parse_rst(
659+
parser,
660+
tabs_path,
661+
"""
662+
.. io-code-block::
663+
664+
.. input:: /test_parser/includes/sample_code.py
665+
:language: python
666+
:linenos:
667+
:start-after: start example 1
668+
:end-before: end example 2
669+
:dedent: 4
670+
:lineno-start: 2
671+
672+
.. output:: /test_parser/includes/sample_code.py
673+
:language: python
674+
:linenos:
675+
:start-after: start example 2
676+
:end-before: end example 2
677+
:dedent: 4
678+
:lineno-start: 6""",
679+
)
680+
page.finish(diagnostics)
681+
assert diagnostics == []
682+
check_ast_testing_string(
683+
page.ast,
684+
"""
685+
<root fileid="test.rst">
686+
<directive name="io-code-block">
687+
<directive name="input" language="python" linenos="True" start-after="start example 1" end-before="end example 2" dedent="4" lineno-start="2">
688+
<text>/test_parser/includes/sample_code.py</text>
689+
<code lang="python" linenos="True" lineno_start="2">print("test dedent")
690+
# end example 1
691+
692+
# start example 2
693+
print("hello world")
694+
</code>
695+
</directive>
696+
<directive name="output" language="python" linenos="True" start-after="start example 2" end-before="end example 2" dedent="4" lineno-start="6">
697+
<text>/test_parser/includes/sample_code.py</text><code lang="python" linenos="True" lineno_start="6">print("hello world")</code>
698+
</directive>
699+
</directive>
700+
</root>""",
701+
)
702+
657703
# Test a io-code-block with incorrect options linenos and emphasize-lines
658704
page, diagnostics = parse_rst(
659705
parser,

0 commit comments

Comments
 (0)