| 
 | 1 | +from __future__ import annotations  | 
 | 2 | + | 
 | 3 | +import glob  | 
 | 4 | +from pathlib import Path  | 
 | 5 | + | 
 | 6 | +from tests.pytest._utils import skip_on_windows  | 
 | 7 | + | 
 | 8 | + | 
 | 9 | +class BadLine:  | 
 | 10 | +    def __init__(self, path: Path, line_number: int, line_text: str):  | 
 | 11 | +        self.path = path  | 
 | 12 | +        self.line_number = line_number  | 
 | 13 | +        self.line_text = line_text  | 
 | 14 | + | 
 | 15 | +    def __repr__(self):  | 
 | 16 | +        return f"{self.path}:{self.line_number} - {self.line_text}"  | 
 | 17 | + | 
 | 18 | + | 
 | 19 | +@skip_on_windows  | 
 | 20 | +def test_shiny_import_itself():  | 
 | 21 | +    """  | 
 | 22 | +    VSCode likes to import from the shiny module itself within the shiny package. While it works... it relies on import magic, not relative imports.  | 
 | 23 | +
  | 
 | 24 | +    Bad: `from shiny import ui`  | 
 | 25 | +    Good: `from . import ui`  | 
 | 26 | +    """  | 
 | 27 | + | 
 | 28 | +    root_here = Path(__file__).parent.parent.parent  | 
 | 29 | +    shiny_files = glob.glob(str(root_here / "shiny" / "**" / "*.py"), recursive=True)  | 
 | 30 | + | 
 | 31 | +    shiny_files = [  | 
 | 32 | +        path  | 
 | 33 | +        for path in shiny_files  | 
 | 34 | +        if "/api-examples/" not in path  | 
 | 35 | +        and "/templates/" not in path  | 
 | 36 | +        and Path(path).is_file()  | 
 | 37 | +    ]  | 
 | 38 | + | 
 | 39 | +    assert len(shiny_files) > 0  | 
 | 40 | + | 
 | 41 | +    # bad_entries: list[tuple[Path, int, str]] = []  | 
 | 42 | +    bad_entries: list[BadLine] = []  | 
 | 43 | + | 
 | 44 | +    # For every python file...  | 
 | 45 | +    for path in shiny_files:  | 
 | 46 | +        path = Path(path)  | 
 | 47 | + | 
 | 48 | +        file_txt = path.read_text(encoding="utf-8")  | 
 | 49 | +        while True:  | 
 | 50 | +            if "\ndef " in file_txt:  | 
 | 51 | +                file_txt = file_txt.split("\ndef ")[0]  | 
 | 52 | +            elif "\nasync def " in file_txt:  | 
 | 53 | +                file_txt = file_txt.split("\nasync def ")[0]  | 
 | 54 | +            elif "\nclass " in file_txt:  | 
 | 55 | +                file_txt = file_txt.split("\nclass ")[0]  | 
 | 56 | +            else:  | 
 | 57 | +                break  | 
 | 58 | + | 
 | 59 | +        for search_txt in ("\nfrom shiny.", "\nfrom shiny ", "\nimport shiny\n"):  | 
 | 60 | +            if search_txt == "\nimport shiny\n" and path.name.endswith("_main.py"):  | 
 | 61 | +                # skip shiny/_main.py file  | 
 | 62 | +                continue  | 
 | 63 | + | 
 | 64 | +            if search_txt in file_txt:  | 
 | 65 | + | 
 | 66 | +                for i, line in enumerate(file_txt.split("\n")):  | 
 | 67 | +                    if line.startswith(search_txt.strip()):  | 
 | 68 | +                        # bad_entries.append((path.relative_to(root_here), i + 1, line))  | 
 | 69 | +                        bad_entries.append(  | 
 | 70 | +                            BadLine(path.relative_to(root_here), i + 1, line)  | 
 | 71 | +                        )  | 
 | 72 | + | 
 | 73 | +    if len(bad_entries) > 0:  | 
 | 74 | +        print("Bad entries found:")  | 
 | 75 | +        for entry in bad_entries:  | 
 | 76 | +            print(entry)  | 
 | 77 | +    # Ensure no bad entries exist  | 
 | 78 | +    assert (  | 
 | 79 | +        len(bad_entries) == 0  | 
 | 80 | +    ), "Unexpected shiny files containing `from shiny.FOO import BAR`, `from shiny import FOO`, or `import shiny`"  | 
0 commit comments