pythonnative 0.28.0__py3-none-any.whl → 0.30.0__py3-none-any.whl

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.
pythonnative/__init__.py CHANGED
@@ -60,7 +60,7 @@ Example:
60
60
  ```
61
61
  """
62
62
 
63
- __version__ = "0.28.0"
63
+ __version__ = "0.30.0"
64
64
 
65
65
  from . import appearance, diagnostics, gestures, images, runtime, sdk
66
66
  from .alerts import Alert
pythonnative/cli/pn.py CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  The console script `pn` (declared in `pyproject.toml`) dispatches to:
4
4
 
5
- - `pn init [name]`: scaffold a new project (``pythonnative.toml`` + ``app/``).
5
+ - `pn init [name]`: scaffold a new project (``pythonnative.toml`` +
6
+ ``app/``) into ``./name/``, or into the current directory when no name
7
+ is given.
6
8
  - `pn doctor [platform]`: diagnose the local toolchain and config.
7
9
  - `pn preview [component]`: render the app in a desktop (Tkinter) window
8
10
  with Fast Refresh, the fast inner dev loop, no device required.
@@ -104,21 +106,62 @@ def _app_id_from_name(name: str) -> str:
104
106
 
105
107
 
106
108
  def init_project(args: argparse.Namespace) -> None:
107
- """Scaffold a new PythonNative project in the current directory.
109
+ """Scaffold a new PythonNative project.
108
110
 
109
- Creates ``app/main.py``, ``pythonnative.toml``, and ``.gitignore``.
110
- Refuses to overwrite existing files unless ``--force`` is passed.
111
+ Given a name, this creates ``./<name>/`` and scaffolds into it. Without
112
+ one, it scaffolds into the current directory and names the project after
113
+ it. Either way it writes ``app/main.py``, ``pythonnative.toml``, and
114
+ ``.gitignore``.
115
+
116
+ The name has to be a single directory name, so the project always lands
117
+ inside the current directory. Anything that reads as a path, such as
118
+ ``a/b``, ``..``, or ``/tmp/app``, is refused, and so is a name that
119
+ resolves somewhere else, such as a symlink to another directory.
120
+
121
+ It won't scaffold into a target directory that already holds files, and it
122
+ won't overwrite any of the three paths above; pass ``--force`` to override
123
+ both. An existing but empty target directory is fine. A plain file at
124
+ ``./<name>`` is always refused, since ``--force`` can't turn it into a
125
+ directory, and ``--force`` lifts neither of the rules above.
111
126
 
112
127
  Args:
113
128
  args: Parsed namespace with ``name`` (optional) and ``force``.
114
129
  """
115
- cwd = Path.cwd()
116
- project_name: str = getattr(args, "name", None) or cwd.name
130
+ name: Optional[str] = getattr(args, "name", None)
117
131
  force: bool = getattr(args, "force", False)
118
132
 
119
- app_dir = cwd / "app"
120
- config_path = cwd / CONFIG_FILENAME
121
- gitignore_path = cwd / ".gitignore"
133
+ # Lexical check, before anything reads the filesystem. ``Path("..").name``
134
+ # is "..", so ".." needs naming explicitly; the rest (absolute, nested,
135
+ # trailing separator, ".") fall out of the name check.
136
+ if name and (name in (os.curdir, os.pardir) or Path(name).name != name):
137
+ print(f"Refusing to treat a path as a project name: {name!r}. Use a single directory name like my_app.")
138
+ sys.exit(1)
139
+
140
+ cwd = Path.cwd()
141
+ target = cwd / name if name else cwd
142
+ project_name: str = name or cwd.name
143
+
144
+ # A lexically clean name can still resolve elsewhere, and ``exists()`` and
145
+ # ``is_dir()`` below follow symlinks. Check containment rather than just
146
+ # ``is_symlink()`` so the whole class is closed, not one spelling of it.
147
+ if name and (target.is_symlink() or target.resolve().parent != cwd.resolve()):
148
+ print(
149
+ f"Refusing to scaffold through a link or outside the current directory: {name}. "
150
+ "Use a plain directory name."
151
+ )
152
+ sys.exit(1)
153
+
154
+ app_dir = target / "app"
155
+ config_path = target / CONFIG_FILENAME
156
+ gitignore_path = target / ".gitignore"
157
+
158
+ if name and target.exists():
159
+ if not target.is_dir():
160
+ print(f"Refusing to overwrite existing file: {name}. Remove it or choose a different name.")
161
+ sys.exit(1)
162
+ if any(target.iterdir()) and not force:
163
+ print(f"Refusing to overwrite existing non-empty directory: {name}/. Use --force to overwrite.")
164
+ sys.exit(1)
122
165
 
123
166
  if not force:
124
167
  existing = [
@@ -142,8 +185,11 @@ def init_project(args: argparse.Namespace) -> None:
142
185
  if force or not gitignore_path.exists():
143
186
  gitignore_path.write_text(_GITIGNORE, encoding="utf-8")
144
187
 
145
- print(f"Initialized PythonNative project in {cwd}.")
146
- print("Next: pn preview (desktop) | pn run android | pn run ios")
188
+ print(f"Initialized PythonNative project in {target}.")
189
+ next_steps = "pn preview (desktop) | pn run android | pn run ios"
190
+ if name:
191
+ next_steps = f"cd {name} | {next_steps}"
192
+ print(f"Next: {next_steps}")
147
193
 
148
194
 
149
195
  # ======================================================================
@@ -873,8 +919,8 @@ def _build_parser() -> argparse.ArgumentParser:
873
919
  subparsers = parser.add_subparsers()
874
920
 
875
921
  parser_init = subparsers.add_parser("init", help="Scaffold a new project")
876
- parser_init.add_argument("name", nargs="?", help="Project name (defaults to current directory name)")
877
- parser_init.add_argument("--force", action="store_true", help="Overwrite existing files if present")
922
+ parser_init.add_argument("name", nargs="?", help="Project name; creates ./<name>/ (default: current directory)")
923
+ parser_init.add_argument("--force", action="store_true", help="Overwrite existing files or a non-empty directory")
878
924
  parser_init.set_defaults(func=init_project)
879
925
 
880
926
  parser_doctor = subparsers.add_parser("doctor", help="Diagnose the local toolchain and config")
@@ -61,8 +61,17 @@ def _which_version(tool: str, version_args: List[str]) -> Optional[str]:
61
61
  return text[0] if text else path
62
62
 
63
63
 
64
+ def _tkinter_available() -> bool:
65
+ """Return whether the host Python can import Tkinter."""
66
+ try:
67
+ import tkinter # noqa: F401
68
+ except ImportError:
69
+ return False
70
+ return True
71
+
72
+
64
73
  def check_common() -> List[CheckResult]:
65
- """Run platform-agnostic checks (interpreter, Pillow).
74
+ """Run platform-agnostic checks (interpreter and optional dependencies).
66
75
 
67
76
  Returns:
68
77
  Check results for the host Python and optional dependencies.
@@ -89,6 +98,17 @@ def check_common() -> List[CheckResult]:
89
98
  "not installed; run: pip install 'pythonnative[build]'",
90
99
  )
91
100
  )
101
+ if _tkinter_available():
102
+ results.append(CheckResult("Tkinter (desktop preview)", OK))
103
+ else:
104
+ results.append(
105
+ CheckResult(
106
+ "Tkinter (desktop preview)",
107
+ WARN,
108
+ "not installed; macOS: brew install python-tk; Debian/Ubuntu: sudo apt-get install python3-tk; "
109
+ "Windows: reinstall Python with the 'tcl/tk' option checked",
110
+ )
111
+ )
92
112
  return results
93
113
 
94
114
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.28.0
3
+ Version: 0.30.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- pythonnative/__init__.py,sha256=cKoregrvgUaM43CDPb7jwsUNTa9Rs7Wh1t7Z8yQeLfE,8680
1
+ pythonnative/__init__.py,sha256=e-p6QNv7CzeBTJnU-SP2ccgdrU4jeVoS0R7sFvlg_S8,8680
2
2
  pythonnative/_ios_log.py,sha256=Oi7V28VxcVoZyrpAirvLeEmUW18McqnU87V4d37Zzlw,2582
3
3
  pythonnative/alerts.py,sha256=mIANysFlaHwL5EqKnvNcyiJN9rGiZi9XDrD9Jpz1RFM,9340
4
4
  pythonnative/animated.py,sha256=A4IOBf_GD6lUn0H8ArFH6URf0-m0PG__8uaxcw6-ECE,60373
@@ -27,7 +27,7 @@ pythonnative/suspense.py,sha256=IxWGXFP3Odriy0uQe9zhrNrObdKer0_WT6v9Vi8Vatg,1466
27
27
  pythonnative/utils.py,sha256=-hwe_YS19ebpjeygdl3dGeVsYzO4G74rYD53svSi0rI,7593
28
28
  pythonnative/virtual_rows.py,sha256=4YK3CeZobW-6F6GCXzGNFfmOaAiigmH4HXtqjYDRbyU,5056
29
29
  pythonnative/cli/__init__.py,sha256=NM1psvKe8jT0vzp8Ak4MMoygZz4P_msk5g-YEsY8xLk,232
30
- pythonnative/cli/pn.py,sha256=Vej6NiT3N_PoNrEZ-Z94o38m-IaoRi9KYx8Gk2IrQTg,34697
30
+ pythonnative/cli/pn.py,sha256=1ZHvuqxW_K9MWn4_1RkVt_oCK-Y0F_bRNq_EYcG57Hc,37022
31
31
  pythonnative/native_modules/__init__.py,sha256=pgigpHuzT-rqwcjlwJvu93_4L8Nozal1HJid0S_JlmM,2636
32
32
  pythonnative/native_modules/app_state.py,sha256=CKAwDQIwoGElSLE3l7Bekk0qRbFVFOTSuSCBybumZEk,2742
33
33
  pythonnative/native_modules/battery.py,sha256=-lJu8LURw-yEObXUyhNq_jHFDa1_33lgzbJzXaGqUL4,4429
@@ -53,7 +53,7 @@ pythonnative/project/android.py,sha256=ynRQwVvv1trFB2eFaUv38-48q4m0_Z4cTbAFEOiKn
53
53
  pythonnative/project/builder.py,sha256=TTN_ymdSyF4wVtSRRwOnSmc91LMn_g_M7_jTI0NtbVU,23946
54
54
  pythonnative/project/config.py,sha256=CmN6VojcuDRVnLwKFnQyd9B20_HL9l0RWDnO91G_JaY,25001
55
55
  pythonnative/project/devices.py,sha256=OkPzqaD6l4xNJer-rKzorAS9IgvsE87g0JK-2V8FIgE,9593
56
- pythonnative/project/doctor.py,sha256=12_7QQCRFuli-INi6_iif8GoRTByDOFxBFgIRFfO0WU,7962
56
+ pythonnative/project/doctor.py,sha256=cNCUWyzu4WBJ9peARIgQ2AyoAGp81NKKM5VkIx7dpFE,8623
57
57
  pythonnative/project/icons.py,sha256=GaXpktQ9v4v2JZPM3ppueNPmwjTuFiLanHJDaZbLGuo,7945
58
58
  pythonnative/project/ios.py,sha256=Xrx2Jh1v4-3ftLkvKAHQuXCA5nuBdKV5nmTTXkDFaNA,13077
59
59
  pythonnative/project/permissions.py,sha256=WyApoqZ0mCTeAtffYbpCGV-bgLvOG7SlPcV7zSO_IV8,14227
@@ -116,9 +116,9 @@ pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/x
116
116
  pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift,sha256=YnwzZx7yXB13xKAXEGNgz17VuhWeqkHTRTtBJ2Vu3_E,1238
117
117
  pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift,sha256=HLRr3cmouRqQeXG13WrZ2XSAmgMgCsNXfZpbwkxeFcs,1385
118
118
  pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift,sha256=f5JrG0uVtLMeJQy26Yyz7Om-JUkT220osqcbeIVkj2g,815
119
- pythonnative-0.28.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
120
- pythonnative-0.28.0.dist-info/METADATA,sha256=nSbCHw3bxTSAKbBHn_52uknD35eTKr9_F1OD9Rcyqt4,11895
121
- pythonnative-0.28.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
122
- pythonnative-0.28.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
123
- pythonnative-0.28.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
124
- pythonnative-0.28.0.dist-info/RECORD,,
119
+ pythonnative-0.30.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
120
+ pythonnative-0.30.0.dist-info/METADATA,sha256=SEAFFisEYUwHRzNmwMffz2ngrHcENxeTnK35CySNegE,11895
121
+ pythonnative-0.30.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
122
+ pythonnative-0.30.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
123
+ pythonnative-0.30.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
124
+ pythonnative-0.30.0.dist-info/RECORD,,