Skip to content

Commit

Permalink
Merge pull request #43 from jepperaskdk/bugfix/multiline-strings
Browse files Browse the repository at this point in the history
Bugfix/multiline strings
  • Loading branch information
jepperaskdk authored Oct 9, 2022
2 parents 08b18ef + d739b97 commit 9b58405
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 16 deletions.
54 changes: 44 additions & 10 deletions pydoctest/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import re
import inspect
import ast
import textwrap

from types import FunctionType, ModuleType
from typing import List, Type, cast
from typing import Any, List, Optional, Type, cast

from pydoc import locate

from pydoctest.exceptions import UnknownTypeException
from pydoctest.exceptions import UnknownTypeException, ParseException


class LocateResult():
Expand Down Expand Up @@ -97,6 +96,38 @@ def visit(self, n: ast.AST) -> None:
super().visit(n)


def dedent_from_first(source_string: str) -> str:
"""
Simple method for dedenting code similar to textwrap.dedent.
A problem with textwrap.dedent is that it only removes common leading whitespace,
which is not enough when a function has multilinestrings that has no leading whitespace.
This function dedents the entire source_string, based on the whitespace/tabs in the first line (i.e. the 'def').
Args:
source_string (str): The source code string to dedent.
Returns:
str: The source code string dedented once.
"""
def get_leading_indents(s: str) -> int:
return len(s) - len(s.lstrip())

if get_leading_indents(source_string) == 0:
return source_string

lines = source_string.splitlines()
if len(lines) == 0:
return source_string

first_leading_indents = get_leading_indents(lines[0])
for i, line in enumerate(lines):
if get_leading_indents(line) != 0:
lines[i] = line[first_leading_indents:]

return '\n'.join(lines)


def get_exceptions_raised(fn: FunctionType, module: ModuleType) -> List[str]:
"""Get exceptions raised in fn.
Expand All @@ -108,22 +139,25 @@ def get_exceptions_raised(fn: FunctionType, module: ModuleType) -> List[str]:
fn (FunctionType): The function to get raised exceptions from.
module (ModuleType): The module in which the function is defined.
Raises:
ParseException: If failing to parse the AST of the source code.
Returns:
List[str]: The list of exceptions thrown.
"""
# TODO: How do we compile functions, without having to check indentation

fn_source = inspect.getsource(fn)
max_indents = 10

# Try to parse, and if IndentationError, dedent up to 10 times.
while max_indents > 0:
tree: Optional[Any] = None
# Try to parse, and if IndentationError, dedent.
for _ in range(2):
try:
tree = ast.parse(fn_source)
break
except IndentationError as e:
fn_source = textwrap.dedent(fn_source)
max_indents -= 1
fn_source = dedent_from_first(fn_source)

if tree is None:
raise ParseException("Failed to parse function source code to detect raised exceptions")

visitor = RaiseVisitor()
visitor.generic_visit(tree)
Expand Down
2 changes: 1 addition & 1 deletion pydoctest/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "0.1.20"
VERSION = "0.1.21"
11 changes: 10 additions & 1 deletion tests/test_class/raises_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,13 @@ def func_with_raise_count_mismatch(self) -> None:
raise ValueError()

if 2 == 5:
raise IndexError()
raise IndexError()

def func_with_raise_multiline_string(self) -> None:
"""
[summary]
"""
multiline_string = """
this has weird indentation
"""
pass
12 changes: 11 additions & 1 deletion tests/test_class/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,14 @@ def test_fail_on_raises_section_dont_fail(self) -> None:
config = Configuration.get_default_configuration()
config.fail_on_raises_section = False
result = validate_function(tests.test_class.raises_class.RaisesClass.func_with_incorrect_raise, config, tests.test_class.incorrect_class)
assert result.result == ResultType.OK
assert result.result == ResultType.OK

# Test multiline strings
def test_raises_on_function_with_multiline_string(self) -> None:
"""
Solves: https://github.com/jepperaskdk/pydoctest/issues/40.
"""
config = Configuration.get_default_configuration()
config.fail_on_raises_section = True
result = validate_function(tests.test_class.raises_class.RaisesClass.func_with_raise_multiline_string, config, tests.test_class.incorrect_class)
assert result.result == ResultType.OK
22 changes: 19 additions & 3 deletions tests/test_utilities/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import pydoctest
from pydoctest.configuration import Configuration

from pydoctest.validation import ResultType, validate_class, validate_function
from pydoctest.main import PyDoctestService
from pydoctest.utilities import get_exceptions_raised, get_type_from_module, is_excluded_path, parse_cli_list, is_excluded_function, is_excluded_class
from pydoctest.validation import validate_function
from pydoctest.utilities import dedent_from_first, get_exceptions_raised, get_type_from_module, is_excluded_path, parse_cli_list, is_excluded_function, is_excluded_class
import tests.test_utilities.example_class


Expand Down Expand Up @@ -104,3 +103,20 @@ def test_is_excluded_function(self) -> None:
assert is_excluded_function("test_function", ["test_function*"])
assert is_excluded_function("test_functionTestClass", ["*st_fu*"])
assert not is_excluded_function("TestClass", ["CrestClass"])

def test_dedent_from_first(self) -> None:
"""
Tests the dedent_from_first function with different inputs.
"""
assert "def test():\n print('hello')" == dedent_from_first(" def test():\n print('hello')")
assert "def test():\n print('hello')" == dedent_from_first(" def test():\n print('hello')")
assert "def test():\n\tprint('hello')" == dedent_from_first("\tdef test():\n\t\tprint('hello')")
assert "def test():\n\tprint('hello')" == dedent_from_first("\t\t\tdef test():\n\t\t\t\tprint('hello')")

# If already indented, should return input
assert "def test():\n\tprint('hello')" == dedent_from_first("def test():\n\tprint('hello')")

# Check single-line functions
assert "def test(): print('hello')" == dedent_from_first("def test(): print('hello')")
assert "def test(): print('hello')" == dedent_from_first(" def test(): print('hello')")
assert "def test(): print('hello')" == dedent_from_first("\tdef test(): print('hello')")

0 comments on commit 9b58405

Please sign in to comment.