44from __future__ import annotations
55
66import ast
7- from pathlib import Path
87import re
8+ import sys
9+ from pathlib import Path
10+ from typing import Literal , cast
911
1012__all__ = ("is_express_app" ,)
1113
@@ -39,8 +41,16 @@ def is_express_app(app: str, app_dir: str | None) -> bool:
3941
4042 try :
4143 # Read the file, parse it, and look for any imports of shiny.express.
42- with open (app_path ) as f :
44+ with open (app_path , encoding = "utf-8" ) as f :
4345 content = f .read ()
46+
47+ # Check for magic comment in the first 1000 characters
48+ forced_mode = find_magic_comment_mode (content [:1000 ])
49+ if forced_mode == "express" :
50+ return True
51+ elif forced_mode == "core" :
52+ return False
53+
4454 tree = ast .parse (content , app_path )
4555 detector = DetectShinyExpressVisitor ()
4656 detector .visit (tree )
@@ -56,25 +66,52 @@ def __init__(self):
5666 super ().__init__ ()
5767 self .found_shiny_express_import = False
5868
59- def visit_Import (self , node : ast .Import ):
69+ def visit_Import (self , node : ast .Import ) -> None :
6070 if any (alias .name == "shiny.express" for alias in node .names ):
6171 self .found_shiny_express_import = True
6272
63- def visit_ImportFrom (self , node : ast .ImportFrom ):
73+ def visit_ImportFrom (self , node : ast .ImportFrom ) -> None :
6474 if node .module == "shiny.express" :
6575 self .found_shiny_express_import = True
6676 elif node .module == "shiny" and any (alias .name == "express" for alias in node .names ):
6777 self .found_shiny_express_import = True
6878
6979 # Visit top-level nodes.
70- def visit_Module (self , node : ast .Module ):
80+ def visit_Module (self , node : ast .Module ) -> None :
7181 super ().generic_visit (node )
7282
7383 # Don't recurse into any nodes, so the we'll only ever look at top-level nodes.
74- def generic_visit (self , node : ast .AST ):
84+ def generic_visit (self , node : ast .AST ) -> None :
7585 pass
7686
7787
88+ def find_magic_comment_mode (content : str ) -> Literal ["core" , "express" ] | None :
89+ """
90+ Look for a magic comment of the form "# shiny_mode: express" or "# shiny_mode:
91+ core".
92+
93+ If a line of the form "# shiny_mode: x" is found, where "x" is not "express" or
94+ "core", then a message will be printed to stderr.
95+
96+ Returns
97+ -------
98+ :
99+ `"express"` if Shiny Express comment is found, `"core"` if Shiny Core comment is
100+ found, and `None` if no magic comment is found.
101+ """
102+ m = re .search (r"^#[ \t]*shiny_mode:[ \t]*(\S*)[ \t]*$" , content , re .MULTILINE )
103+ if m is not None :
104+ shiny_mode = cast (str , m .group (1 ))
105+ if shiny_mode in ("express" , "core" ):
106+ # The "type: ignore" is needed for mypy, which is used on some projects that
107+ # use duplicates of this code.
108+ return shiny_mode # type: ignore
109+ else :
110+ print (f'Invalid shiny_mode: "{ shiny_mode } "' , file = sys .stderr )
111+
112+ return None
113+
114+
78115def escape_to_var_name (x : str ) -> str :
79116 """
80117 Given a string, escape it to a valid Python variable name which contains
0 commit comments