@@ -50,6 +50,7 @@ def run_quickstart(
5050 app_type : str ,
5151 name : str ,
5252 * ,
53+ python_version : typing .Optional [str ] = None ,
5354 cwd : typing .Optional [pathlib .Path ] = None ,
5455) -> pathlib .Path :
5556 """Scaffold a new Connect project of ``app_type`` named ``name``.
@@ -61,6 +62,12 @@ def run_quickstart(
6162
6263 :param str app_type: one of the supported CLI types.
6364 :param str name: project name; must satisfy the project-name rule above.
65+ :param str python_version: optional ``requires-python`` control. A value
66+ that begins with a specifier operator (e.g. ``>=3.11`` or
67+ ``>=3.11,<3.14``) is used verbatim. A bare version is padded to at
68+ most three segments: ``3.10`` -> ``==3.10.*`` (any 3.10.x) and
69+ ``3.11.14`` -> ``==3.11.14`` (exact). Defaults to ``>=<major.minor>``
70+ of the interpreter running ``rsconnect``.
6471 :param pathlib.Path cwd: override the working directory (testing hook);
6572 defaults to :func:`pathlib.Path.cwd`.
6673 """
@@ -82,13 +89,26 @@ def run_quickstart(
8289 # for direct API callers only.
8390 spec = lookup_template (app_type )
8491
92+ # ``--python`` controls ``requires-python``. A value that already starts
93+ # with a specifier operator (``>=3.11``, ``>=3.11,<3.14``, ...) is used
94+ # verbatim. A bare version is padded to at most three segments so the
95+ # ``.*`` wildcard appears only when a patch level is omitted: ``3.10`` ->
96+ # ``==3.10.*`` (any 3.10.x), ``3.11.14`` -> ``==3.11.14`` (exact).
97+ # Without ``--python`` we track the running interpreter's ``major.minor``.
98+ if python_version is None :
99+ requires_python = _REQUIRES_PYTHON
100+ elif python_version [:1 ] in {"=" , "<" , ">" , "!" , "~" }:
101+ requires_python = python_version
102+ else :
103+ requires_python = "==" + "." .join ((python_version .split ("." ) + ["*" ])[:3 ])
104+
85105 # Atomicity: after ``mkdir`` succeeds, any failure in the rest of the
86106 # pipeline must remove ``./<name>/`` so the user sees "all or nothing."
87107 # ``BaseException`` catches ``KeyboardInterrupt`` too (a Ctrl-C
88108 # mid-``uv sync`` is the most likely real-world failure mode).
89109 target .mkdir ()
90110 try :
91- _scaffold (target , name = name , spec = spec )
111+ _scaffold (target , name = name , spec = spec , requires_python = requires_python )
92112 _install_venv (target )
93113 except BaseException :
94114 shutil .rmtree (target , ignore_errors = True )
@@ -335,7 +355,7 @@ def lookup_template(app_type: str) -> TemplateSpec:
335355# ---------------------------------------------------------------------------
336356
337357
338- def _scaffold (target : pathlib .Path , * , name : str , spec : TemplateSpec ) -> None :
358+ def _scaffold (target : pathlib .Path , * , name : str , spec : TemplateSpec , requires_python : str ) -> None :
339359 """Write every file the scaffolded project should contain.
340360
341361 Filesystem-generation phase: the three always-present files
@@ -344,7 +364,9 @@ def _scaffold(target: pathlib.Path, *, name: str, spec: TemplateSpec) -> None:
344364 ``target``'s creation and rollback, so this helper writes into an
345365 existing directory.
346366 """
347- (target / "pyproject.toml" ).write_text (_render_pyproject (name = name , spec = spec ), encoding = "utf-8" )
367+ (target / "pyproject.toml" ).write_text (
368+ _render_pyproject (name = name , spec = spec , requires_python = requires_python ), encoding = "utf-8"
369+ )
348370 (target / ".gitignore" ).write_text (_GITIGNORE_BODY , encoding = "utf-8" )
349371 (target / "README.md" ).write_text (_render_readme (name = name , spec = spec ), encoding = "utf-8" )
350372 for file_spec in spec .source_files :
@@ -389,13 +411,13 @@ def _load_template(path: str) -> str:
389411"""
390412
391413
392- def _render_pyproject (* , name : str , spec : TemplateSpec ) -> str :
414+ def _render_pyproject (* , name : str , spec : TemplateSpec , requires_python : str ) -> str :
393415 # The per-mode template owns the literal TOML, including ``app_mode``,
394416 # ``entrypoint`` and the dependency list. Only ``$name`` (project name)
395- # and ``$requires_python`` (computed from the running interpreter) vary
396- # at scaffold time.
417+ # and ``$requires_python`` (from ``--python`` or the running interpreter)
418+ # vary at scaffold time.
397419 return string .Template (_load_template (spec .pyproject_template )).substitute (
398- name = name , requires_python = _REQUIRES_PYTHON
420+ name = name , requires_python = requires_python
399421 )
400422
401423
@@ -450,14 +472,15 @@ def _install_venv(target: pathlib.Path) -> None:
450472
451473
452474def _emit_summary (target : pathlib .Path , * , name : str , spec : TemplateSpec ) -> None :
453- """Print the confirmation, local-run, deploy, and notes lines.
475+ """Print the confirmation, cd, local-run, deploy, and notes lines.
454476
455477 Uses :func:`click.echo` for consistency with the rest of the CLI; the
456478 same commands are written into the project's ``README.md`` by
457479 :func:`_render_readme` so stdout and on-disk docs agree.
458480 """
459- click .echo (f"Project { target .name } / created." )
481+ click .echo (f"\n Project { target .name } / created." )
482+ click .echo (f"To get started: cd { name } " )
460483 click .echo (f"To run locally: { _format_local_run (spec , name = name )} " )
461- click .echo (f "To deploy: rsconnect deploy pyproject { name } " )
484+ click .echo ("To deploy: rsconnect deploy pyproject . " )
462485 for note in spec .notes :
463486 click .echo (f"Note: { note } " )
0 commit comments