create-awesome-python-app 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/PKG-INFO +2 -2
  2. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/pyproject.toml +2 -2
  3. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/__init__.py +1 -1
  4. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/catalog.py +26 -18
  5. create_awesome_python_app-0.2.4/src/create_awesome_python_app/prompt_style.py +113 -0
  6. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog_resolve.py +15 -9
  7. create_awesome_python_app-0.2.3/src/create_awesome_python_app/prompt_style.py +0 -61
  8. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/.gitignore +0 -0
  9. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/README.md +0 -0
  10. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/assets/hero.svg +0 -0
  11. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/cache.py +0 -0
  12. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/src/create_awesome_python_app/cli.py +0 -0
  13. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cache_cmds.py +0 -0
  14. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cache_env.py +0 -0
  15. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog.py +0 -0
  16. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_catalog_fetch.py +0 -0
  17. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cli.py +0 -0
  18. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_cpa_templates_integration.py +0 -0
  19. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_flags.py +0 -0
  20. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_interactive.py +0 -0
  21. {create_awesome_python_app-0.2.3 → create_awesome_python_app-0.2.4}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: create-awesome-python-app
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Composable scaffolding CLI for production-ready Python apps
5
5
  Project-URL: Homepage, https://github.com/Create-Python-App/create-python-app
6
6
  Project-URL: Repository, https://github.com/Create-Python-App/create-python-app
@@ -8,7 +8,7 @@ Project-URL: Issues, https://github.com/Create-Python-App/create-python-app/issu
8
8
  Project-URL: Changelog, https://github.com/Create-Python-App/create-python-app/blob/main/CHANGELOG.md
9
9
  License-Expression: MIT
10
10
  Requires-Python: >=3.12
11
- Requires-Dist: create-python-app-core>=0.2.3
11
+ Requires-Dist: create-python-app-core>=0.2.4
12
12
  Requires-Dist: questionary>=2.1.1
13
13
  Requires-Dist: rich>=15.0.0
14
14
  Requires-Dist: typer>=0.27.0
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "create-awesome-python-app"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Composable scaffolding CLI for production-ready Python apps"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  license = "MIT"
8
8
  dependencies = [
9
- "create-python-app-core>=0.2.3",
9
+ "create-python-app-core>=0.2.4",
10
10
  "questionary>=2.1.1",
11
11
  "rich>=15.0.0",
12
12
  "typer>=0.27.0",
@@ -1,3 +1,3 @@
1
1
  """Create Awesome Python App CLI."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.2.4"
@@ -16,7 +16,10 @@ from rich.console import Console
16
16
  from rich.table import Table
17
17
 
18
18
  from create_awesome_python_app import __version__
19
- from create_awesome_python_app.prompt_style import bold, color_category, dim
19
+ from create_awesome_python_app.prompt_style import (
20
+ custom_template_title,
21
+ template_title_tokens,
22
+ )
20
23
 
21
24
  console = Console(stderr=True)
22
25
 
@@ -27,7 +30,7 @@ CUSTOM_TEMPLATE_SENTINEL = "__custom_template__"
27
30
  class TemplateChoice:
28
31
  """Searchable interactive template choice."""
29
32
 
30
- title: str
33
+ title: Any
31
34
  value: str
32
35
  search: str
33
36
 
@@ -236,35 +239,40 @@ def build_template_choices(data: dict[str, Any]) -> list[TemplateChoice]:
236
239
  category_name = categories.get(category_slug, category_slug)
237
240
  badge = short_category_label(category_name).ljust(10)[:10]
238
241
  slug = str(template.get("slug", ""))
239
- labels = template.get("labels", [])
240
- label_suffix = ""
241
- if isinstance(labels, list) and labels:
242
- label_suffix = dim(" · " + ", ".join(str(label) for label in labels[:3]))
242
+ labels_raw = template.get("labels", [])
243
+ labels = (
244
+ [str(label) for label in labels_raw[:3]]
245
+ if isinstance(labels_raw, list)
246
+ else []
247
+ )
243
248
  description = str(template.get("description", "")).strip()
244
- # Keep slug + short description in the title so select(use_search_filter)
245
- # can match them (filter scans Choice.title only).
246
- description_suffix = dim(f" — {description}") if description else ""
247
249
  name = str(template.get("name", slug))
248
- # ANSI is OK here: questionary.select renders titles as terminal text.
249
- # Do not pass these titles to autocomplete (HTML match highlighting).
250
- title = (
251
- f"{color_category(category_slug, badge)} "
252
- f"{bold(name)} ({slug})"
253
- f"{label_suffix}{description_suffix}"
250
+ search = _search_text(template, category_name)
251
+ # FormattedText tokens (not raw ANSI): select() prints str titles
252
+ # literally, which showed ^[[1;94m… in terminals.
253
+ title = template_title_tokens(
254
+ category_slug=category_slug,
255
+ badge=badge,
256
+ name=name,
257
+ slug=slug,
258
+ labels=labels,
259
+ description=description,
260
+ search=search,
254
261
  )
255
262
  choices.append(
256
263
  TemplateChoice(
257
264
  title=title,
258
265
  value=template_url,
259
- search=_search_text(template, category_name),
266
+ search=search,
260
267
  )
261
268
  )
262
269
 
270
+ custom_search = "custom own template url github file"
263
271
  choices.append(
264
272
  TemplateChoice(
265
- title=" " * 12 + dim("Use my own template URL"),
273
+ title=custom_template_title(custom_search),
266
274
  value=CUSTOM_TEMPLATE_SENTINEL,
267
- search="custom own template url github file",
275
+ search=custom_search,
268
276
  )
269
277
  )
270
278
  return choices
@@ -0,0 +1,113 @@
1
+ """High-contrast questionary styles for CPA interactive prompts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ from questionary import Style
9
+
10
+ # Lighter brand blues/greens than docs hex so selected rows stay readable on
11
+ # dark terminals (slate backgrounds common in Arch/Ghostty/Alacritty).
12
+ CPA_PROMPT_STYLE = Style.from_dict(
13
+ {
14
+ "qmark": "fg:#60a5fa bold",
15
+ "question": "bold fg:#f8fafc",
16
+ "answer": "fg:#4ade80 bold",
17
+ "pointer": "fg:#60a5fa bold",
18
+ "highlighted": "fg:#0f172a bg:#60a5fa bold",
19
+ "selected": "fg:#4ade80 bold",
20
+ "separator": "fg:#94a3b8",
21
+ "instruction": "fg:#94a3b8",
22
+ "text": "fg:#e2e8f0",
23
+ "disabled": "fg:#64748b italic",
24
+ "checkbox": "fg:#60a5fa",
25
+ "checkbox-selected": "fg:#4ade80 bold",
26
+ }
27
+ )
28
+
29
+ # Bright prompt_toolkit style strings (not raw ANSI — select() prints str titles
30
+ # literally, so escapes show as ^[[...m unless titles are FormattedText tokens).
31
+ _CATEGORY_STYLES = (
32
+ "fg:#facc15 bold", # yellow
33
+ "fg:#4ade80 bold", # green
34
+ "fg:#22d3ee bold", # cyan
35
+ "fg:#e879f9 bold", # magenta
36
+ "fg:#60a5fa bold", # blue
37
+ )
38
+
39
+
40
+ class SearchableFormattedText(list):
41
+ """FormattedText tokens with ``.lower()`` for questionary search filter.
42
+
43
+ ``select(use_search_filter=True)`` does ``needle in choice.title.lower()``.
44
+ A plain token list has no ``.lower()``; this keeps filter working while
45
+ titles render as styled FormattedText.
46
+ """
47
+
48
+ def __init__(self, tokens: list[tuple[str, str]], search: str) -> None:
49
+ super().__init__(tokens)
50
+ self._search = search
51
+
52
+ def lower(self) -> str:
53
+ return self._search.lower()
54
+
55
+
56
+ def colors_enabled() -> bool:
57
+ return not os.environ.get("NO_COLOR")
58
+
59
+
60
+ def category_style(slug: str) -> str:
61
+ idx = sum(ord(char) for char in slug) % len(_CATEGORY_STYLES)
62
+ return _CATEGORY_STYLES[idx]
63
+
64
+
65
+ def plain_title_text(title: Any) -> str:
66
+ """Join FormattedText token text (or return a plain string title)."""
67
+ if isinstance(title, list):
68
+ return "".join(str(token[1]) for token in title)
69
+ return str(title)
70
+
71
+
72
+ def template_title_tokens(
73
+ *,
74
+ category_slug: str,
75
+ badge: str,
76
+ name: str,
77
+ slug: str,
78
+ labels: list[str],
79
+ description: str,
80
+ search: str,
81
+ ) -> SearchableFormattedText | str:
82
+ """Build a select-safe title: FormattedText when colors on, else plain str."""
83
+ label_suffix = ""
84
+ if labels:
85
+ label_suffix = " · " + ", ".join(labels[:3])
86
+ description_suffix = f" — {description}" if description else ""
87
+ plain = f"{badge} {name} ({slug}){label_suffix}{description_suffix}"
88
+
89
+ if not colors_enabled():
90
+ return plain
91
+
92
+ tokens: list[tuple[str, str]] = [
93
+ (category_style(category_slug), badge),
94
+ ("", " "),
95
+ ("bold", name),
96
+ ("class:instruction", f" ({slug})"),
97
+ ]
98
+ if label_suffix:
99
+ tokens.append(("class:instruction", label_suffix))
100
+ if description_suffix:
101
+ tokens.append(("fg:#94a3b8", description_suffix))
102
+ return SearchableFormattedText(tokens, search=search or plain)
103
+
104
+
105
+ def custom_template_title(search: str) -> SearchableFormattedText | str:
106
+ label = "Use my own template URL"
107
+ plain = " " * 12 + label
108
+ if not colors_enabled():
109
+ return plain
110
+ return SearchableFormattedText(
111
+ [("", " " * 12), ("italic fg:#94a3b8", label)],
112
+ search=search or plain,
113
+ )
@@ -17,6 +17,10 @@ from create_awesome_python_app.catalog import (
17
17
  short_category_label,
18
18
  validate_extension_compatibility,
19
19
  )
20
+ from create_awesome_python_app.prompt_style import (
21
+ SearchableFormattedText,
22
+ plain_title_text,
23
+ )
20
24
 
21
25
  SAMPLE_CATALOG = {
22
26
  "templates": [
@@ -99,19 +103,18 @@ def test_build_template_choices_are_searchable() -> None:
99
103
  choices = build_template_choices(catalog)
100
104
  first = choices[0]
101
105
  assert first.value == "file:///templates/fastapi"
102
- assert "FastAPI Starter" in first.title
103
- assert "OpenAPI" in first.title
104
- assert "uv" in first.title
106
+ title_text = plain_title_text(first.title)
107
+ assert "FastAPI Starter" in title_text
108
+ assert "OpenAPI" in title_text
109
+ assert "uv" in title_text
105
110
  assert "openapi" in first.search
106
111
  assert "backend" in first.search
107
112
  assert "uv" in first.search
108
113
  assert choices[-1].value == CUSTOM_TEMPLATE_SENTINEL
109
114
 
110
115
 
111
- def test_template_choice_titles_include_bright_category_ansi(
112
- monkeypatch,
113
- ) -> None:
114
- """select() can render ANSI; badges use bright bold codes for contrast."""
116
+ def test_template_choice_titles_use_formatted_text(monkeypatch) -> None:
117
+ """select() needs FormattedText tokens; raw ANSI shows as ^[[…m."""
115
118
  monkeypatch.delenv("NO_COLOR", raising=False)
116
119
  catalog = {
117
120
  "categories": [
@@ -127,8 +130,10 @@ def test_template_choice_titles_include_bright_category_ansi(
127
130
  ],
128
131
  }
129
132
  title = build_template_choices(catalog)[0].title
130
- assert "\033[" in title
131
- assert "FastAPI Starter" in title
133
+ assert isinstance(title, SearchableFormattedText)
134
+ assert "\033" not in plain_title_text(title)
135
+ assert "FastAPI Starter" in plain_title_text(title)
136
+ assert "fastapi" in title.lower()
132
137
 
133
138
 
134
139
  def test_template_choice_titles_respect_no_color(monkeypatch) -> None:
@@ -147,6 +152,7 @@ def test_template_choice_titles_respect_no_color(monkeypatch) -> None:
147
152
  ],
148
153
  }
149
154
  title = build_template_choices(catalog)[0].title
155
+ assert isinstance(title, str)
150
156
  assert "\033" not in title
151
157
  assert "FastAPI Starter" in title
152
158
 
@@ -1,61 +0,0 @@
1
- """High-contrast questionary styles for CPA interactive prompts."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
-
7
- from questionary import Style
8
-
9
- # Lighter brand blues/greens than docs hex so selected rows stay readable on
10
- # dark terminals (slate backgrounds common in Arch/Ghostty/Alacritty).
11
- CPA_PROMPT_STYLE = Style.from_dict(
12
- {
13
- "qmark": "fg:#60a5fa bold",
14
- "question": "bold fg:#f8fafc",
15
- "answer": "fg:#4ade80 bold",
16
- "pointer": "fg:#60a5fa bold",
17
- "highlighted": "fg:#0f172a bg:#60a5fa bold",
18
- "selected": "fg:#4ade80 bold",
19
- "separator": "fg:#94a3b8",
20
- "instruction": "fg:#94a3b8",
21
- "text": "fg:#e2e8f0",
22
- "disabled": "fg:#64748b italic",
23
- "checkbox": "fg:#60a5fa",
24
- "checkbox-selected": "fg:#4ade80 bold",
25
- }
26
- )
27
-
28
-
29
- def colors_enabled() -> bool:
30
- return not os.environ.get("NO_COLOR")
31
-
32
-
33
- def ansi(code: str, text: str) -> str:
34
- """Wrap *text* in an ANSI SGR sequence when colors are enabled."""
35
- if not colors_enabled():
36
- return text
37
- return f"\033[{code}m{text}\033[0m"
38
-
39
-
40
- # Bold bright ANSI — readable on dark terminals; select() renders these safely
41
- # (unlike autocomplete, which HTML-parses choice text).
42
- _CATEGORY_PALETTE = (
43
- "1;93", # bright yellow
44
- "1;92", # bright green
45
- "1;96", # bright cyan
46
- "1;95", # bright magenta
47
- "1;94", # bright blue
48
- )
49
-
50
-
51
- def color_category(slug: str, label: str) -> str:
52
- idx = sum(ord(char) for char in slug) % len(_CATEGORY_PALETTE)
53
- return ansi(_CATEGORY_PALETTE[idx], label)
54
-
55
-
56
- def bold(text: str) -> str:
57
- return ansi("1", text)
58
-
59
-
60
- def dim(text: str) -> str:
61
- return ansi("2", text)