pythonnative 0.37.0__py3-none-any.whl → 0.38.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.
Files changed (47) hide show
  1. pythonnative/__init__.py +9 -1
  2. pythonnative/alerts.py +6 -14
  3. pythonnative/cli/pn.py +96 -10
  4. pythonnative/components/_base.py +31 -0
  5. pythonnative/components/controls.py +19 -16
  6. pythonnative/components/layout.py +5 -8
  7. pythonnative/components/lists.py +6 -6
  8. pythonnative/gestures.py +27 -6
  9. pythonnative/native_modules/__init__.py +19 -0
  10. pythonnative/native_modules/battery.py +13 -14
  11. pythonnative/native_modules/biometrics.py +12 -14
  12. pythonnative/native_modules/camera.py +10 -6
  13. pythonnative/native_modules/clipboard.py +9 -11
  14. pythonnative/native_modules/file_system.py +81 -172
  15. pythonnative/native_modules/haptics.py +14 -10
  16. pythonnative/native_modules/linking.py +9 -8
  17. pythonnative/native_modules/location.py +10 -12
  18. pythonnative/native_modules/net_info.py +1 -4
  19. pythonnative/native_modules/notifications.py +25 -32
  20. pythonnative/native_modules/permissions.py +70 -22
  21. pythonnative/native_modules/secure_store.py +16 -20
  22. pythonnative/native_modules/share.py +5 -5
  23. pythonnative/navigation/__init__.py +2 -1
  24. pythonnative/navigation/hooks.py +46 -7
  25. pythonnative/navigation/state.py +30 -9
  26. pythonnative/project/android.py +23 -0
  27. pythonnative/project/builder.py +64 -35
  28. pythonnative/project/config.py +84 -24
  29. pythonnative/project/deps.py +703 -0
  30. pythonnative/project/doctor.py +53 -4
  31. pythonnative/project/runtime_assets.py +13 -14
  32. pythonnative/style.py +94 -44
  33. pythonnative/templates/android_template/app/build.gradle +5 -2
  34. pythonnative/templates/android_template/build.gradle +7 -4
  35. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
  36. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +15 -4
  37. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +25 -10
  38. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +3 -2
  39. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +35 -9
  40. pythonnative/utils.py +22 -27
  41. pythonnative/virtual_rows.py +10 -20
  42. {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/METADATA +8 -8
  43. {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/RECORD +47 -46
  44. {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/WHEEL +0 -0
  45. {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/entry_points.txt +0 -0
  46. {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/licenses/LICENSE +0 -0
  47. {pythonnative-0.37.0.dist-info → pythonnative-0.38.0.dist-info}/top_level.txt +0 -0
@@ -38,8 +38,12 @@ class Camera:
38
38
  ``allow_editing``, ...). Unknown keys are ignored.
39
39
 
40
40
  Returns:
41
- The saved image path, or ``None`` if the user cancelled or
42
- no camera is available.
41
+ The saved image path, or ``None`` if the user cancelled (or
42
+ there is no camera to present, as on desktop).
43
+
44
+ Raises:
45
+ NativeModuleError: If the picker can't be presented, for
46
+ example because another picker is already open.
43
47
  """
44
48
  return await _launch("take_photo", options)
45
49
 
@@ -49,13 +53,13 @@ class Camera:
49
53
 
50
54
  Returns:
51
55
  The selected image path, or ``None`` if the user cancelled.
56
+
57
+ Raises:
58
+ NativeModuleError: If the picker can't be presented.
52
59
  """
53
60
  return await _launch("pick_from_gallery", options)
54
61
 
55
62
 
56
63
  async def _launch(method: str, options: Any) -> Optional[str]:
57
- try:
58
- result = await native_module("Camera").call_async(method, **options)
59
- except Exception:
60
- return None
64
+ result = await native_module("Camera").call_async(method, **options)
61
65
  return str(result) if result else None
@@ -2,7 +2,8 @@
2
2
 
3
3
  [`Clipboard`][pythonnative.Clipboard] reads and writes the system
4
4
  pasteboard through the native ``Clipboard`` module (``UIPasteboard``
5
- on iOS, ``ClipboardManager`` on Android). All methods are synchronous.
5
+ on iOS, ``ClipboardManager`` on Android). The pasteboard lives in
6
+ process memory on both platforms, so every method is synchronous.
6
7
 
7
8
  Off device the module is a process-local string buffer, which keeps it
8
9
  usable in ``pn preview`` and unit tests.
@@ -18,28 +19,25 @@ Example:
18
19
 
19
20
  from __future__ import annotations
20
21
 
21
- from .. import diagnostics
22
22
  from .registry import native_module
23
23
 
24
24
 
25
25
  class Clipboard:
26
- """System clipboard interface (synchronous)."""
26
+ """System clipboard interface (synchronous).
27
+
28
+ Raises:
29
+ NativeModuleError: If the native module reports a failure.
30
+ """
27
31
 
28
32
  @staticmethod
29
33
  def set_string(text: str) -> None:
30
34
  """Copy ``text`` onto the system clipboard."""
31
- try:
32
- native_module("Clipboard").call("set_string", text="" if text is None else str(text))
33
- except Exception:
34
- diagnostics.swallowed("clipboard.set_string")
35
+ native_module("Clipboard").call("set_string", text="" if text is None else str(text))
35
36
 
36
37
  @staticmethod
37
38
  def get_string() -> str:
38
39
  """Return the current clipboard string (``""`` when empty)."""
39
- try:
40
- value = native_module("Clipboard").call("get_string")
41
- except Exception:
42
- return ""
40
+ value = native_module("Clipboard").call("get_string")
43
41
  return "" if value is None else str(value)
44
42
 
45
43
  @staticmethod
@@ -1,9 +1,19 @@
1
- """Cross-platform app-scoped file I/O.
2
-
3
- Provides static helpers for reading, writing, and deleting files in the
4
- app's sandboxed storage area. Relative paths are resolved against
5
- [`FileSystem.app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir];
6
- absolute paths are used as-is.
1
+ """App-scoped file I/O.
2
+
3
+ [`FileSystem`][pythonnative.FileSystem] answers one question the
4
+ standard library can't, "where may this app write?", and then gets out
5
+ of the way: [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir]
6
+ comes from the native ``Device`` module, and
7
+ [`path`][pythonnative.native_modules.file_system.FileSystem.path] turns
8
+ an app-relative name into a ``pathlib.Path`` you use like any other.
9
+ The read/write helpers are thin conveniences over that path; they
10
+ raise the same ``OSError`` subclasses ``open`` and ``os`` do
11
+ (``FileNotFoundError``, ``PermissionError``, ...) rather than hiding
12
+ them behind ``None`` and ``False``.
13
+
14
+ Relative paths are resolved against ``app_dir``; absolute paths are used
15
+ as-is. Everything here is synchronous: it is local disk I/O on the
16
+ calling thread, exactly like the standard library.
7
17
 
8
18
  Example:
9
19
  ```python
@@ -11,36 +21,44 @@ Example:
11
21
 
12
22
  FileSystem.write_text("notes/today.txt", "Hello, file system!")
13
23
  print(FileSystem.read_text("notes/today.txt"))
24
+
25
+ # Or work with the Path directly:
26
+ notes = FileSystem.path("notes")
27
+ for entry in sorted(notes.iterdir()):
28
+ print(entry.name)
14
29
  ```
15
30
  """
16
31
 
32
+ from __future__ import annotations
33
+
17
34
  import os
18
- from typing import Any, Optional
35
+ from pathlib import Path
36
+ from typing import Any, List, Optional, Union
19
37
 
20
38
  from .registry import native_module
21
39
 
40
+ PathLike = Union[str, "os.PathLike[str]"]
41
+
22
42
  _app_dir_cache: Optional[str] = None
23
43
 
24
44
 
25
45
  class FileSystem:
26
46
  """App-scoped file I/O.
27
47
 
28
- Every instance method operates on either an absolute path or a path
29
- relative to
30
- [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir].
31
- Errors are swallowed and reported as falsy return values (`None`
32
- for readers, `False` for writers) so callers can treat the API as
33
- best-effort.
48
+ Every helper accepts an absolute path or a path relative to
49
+ [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir],
50
+ and raises ``OSError`` (or a subclass) when the operation fails,
51
+ like the standard library it wraps.
34
52
  """
35
53
 
36
54
  @staticmethod
37
55
  def app_dir() -> str:
38
56
  """Return the app's writable data directory.
39
57
 
40
- On Android the result is `Context.getFilesDir()`. On iOS it is
41
- the user's Documents directory. On a desktop machine without
42
- either runtime, a `.pythonnative_data` directory is created
43
- under the user's home folder. The value comes from the native
58
+ On Android the result is ``Context.getFilesDir()``. On iOS it is
59
+ the app's Documents directory. On a desktop machine without
60
+ either runtime, a ``.pythonnative_data`` directory under the
61
+ user's home folder is used. The value comes from the native
44
62
  ``Device`` module's ``info()`` and is cached after the first
45
63
  call.
46
64
 
@@ -49,188 +67,79 @@ class FileSystem:
49
67
  """
50
68
  global _app_dir_cache
51
69
  if _app_dir_cache is None:
52
- path: Optional[str] = None
53
- try:
54
- info = native_module("Device").call("info")
55
- if isinstance(info, dict):
56
- path = info.get("app_dir")
57
- except Exception:
58
- path = None
70
+ info = native_module("Device").call("info")
71
+ path = info.get("app_dir") if isinstance(info, dict) else None
59
72
  _app_dir_cache = str(path) if path else os.path.join(os.path.expanduser("~"), ".pythonnative_data")
60
73
  return _app_dir_cache
61
74
 
62
75
  @staticmethod
63
- def read_text(path: str, encoding: str = "utf-8") -> Optional[str]:
64
- """Read a text file.
76
+ def path(path: PathLike = "") -> Path:
77
+ """Resolve ``path`` against ``app_dir`` and return it as a ``pathlib.Path``.
65
78
 
66
- Args:
67
- path: Absolute path or path relative to
68
- [`app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir].
69
- encoding: Text encoding (default `"utf-8"`).
70
-
71
- Returns:
72
- File contents as a `str`, or `None` if the file cannot be
73
- read.
79
+ Absolute paths are returned unchanged. With no argument, returns
80
+ ``app_dir`` itself.
74
81
  """
75
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
76
- try:
77
- with open(full, encoding=encoding) as f:
78
- return f.read()
79
- except OSError:
80
- return None
82
+ candidate = Path(path)
83
+ return candidate if candidate.is_absolute() else Path(FileSystem.app_dir()) / candidate
81
84
 
82
85
  @staticmethod
83
- def write_text(path: str, content: str, encoding: str = "utf-8") -> bool:
84
- """Write a text file, creating parent directories as needed.
85
-
86
- Args:
87
- path: Absolute or `app_dir`-relative path.
88
- content: String to write.
89
- encoding: Text encoding (default `"utf-8"`).
90
-
91
- Returns:
92
- `True` on success, `False` on `OSError`.
93
- """
94
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
95
- try:
96
- os.makedirs(os.path.dirname(full), exist_ok=True)
97
- with open(full, "w", encoding=encoding) as f:
98
- f.write(content)
99
- return True
100
- except OSError:
101
- return False
86
+ def read_text(path: PathLike, encoding: str = "utf-8") -> str:
87
+ """Read a text file; raises ``FileNotFoundError`` and friends like ``open`` does."""
88
+ return FileSystem.path(path).read_text(encoding=encoding)
102
89
 
103
90
  @staticmethod
104
- def exists(path: str) -> bool:
105
- """Return whether a file or directory exists.
106
-
107
- Args:
108
- path: Absolute or `app_dir`-relative path.
109
-
110
- Returns:
111
- `True` if the path exists.
112
- """
113
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
114
- return os.path.exists(full)
91
+ def write_text(path: PathLike, content: str, encoding: str = "utf-8") -> None:
92
+ """Write a text file, creating parent directories as needed."""
93
+ target = FileSystem.path(path)
94
+ target.parent.mkdir(parents=True, exist_ok=True)
95
+ target.write_text(content, encoding=encoding)
115
96
 
116
97
  @staticmethod
117
- def delete(path: str) -> bool:
118
- """Delete a single file.
119
-
120
- Args:
121
- path: Absolute or `app_dir`-relative path.
122
-
123
- Returns:
124
- `True` on success, `False` on `OSError`.
125
- """
126
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
127
- try:
128
- os.remove(full)
129
- return True
130
- except OSError:
131
- return False
98
+ def read_bytes(path: PathLike) -> bytes:
99
+ """Read a binary file."""
100
+ return FileSystem.path(path).read_bytes()
132
101
 
133
102
  @staticmethod
134
- def list_dir(path: str = "") -> list:
135
- """List the entries in a directory.
136
-
137
- Args:
138
- path: Absolute or `app_dir`-relative path. Defaults to the
139
- app data directory itself.
140
-
141
- Returns:
142
- A list of entry names, or an empty list on error.
143
- """
144
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
145
- try:
146
- return os.listdir(full)
147
- except OSError:
148
- return []
103
+ def write_bytes(path: PathLike, data: bytes) -> None:
104
+ """Write a binary file, creating parent directories as needed."""
105
+ target = FileSystem.path(path)
106
+ target.parent.mkdir(parents=True, exist_ok=True)
107
+ target.write_bytes(data)
149
108
 
150
109
  @staticmethod
151
- def read_bytes(path: str) -> Optional[bytes]:
152
- """Read a binary file.
153
-
154
- Args:
155
- path: Absolute or `app_dir`-relative path.
156
-
157
- Returns:
158
- File contents as `bytes`, or `None` if the file cannot be
159
- read.
160
- """
161
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
162
- try:
163
- with open(full, "rb") as f:
164
- return f.read()
165
- except OSError:
166
- return None
110
+ def exists(path: PathLike) -> bool:
111
+ """Return whether a file or directory exists."""
112
+ return FileSystem.path(path).exists()
167
113
 
168
114
  @staticmethod
169
- def write_bytes(path: str, data: bytes) -> bool:
170
- """Write a binary file, creating parent directories as needed.
115
+ def delete(path: PathLike, *, missing_ok: bool = False) -> None:
116
+ """Delete a single file.
171
117
 
172
118
  Args:
173
- path: Absolute or `app_dir`-relative path.
174
- data: Bytes to write.
175
-
176
- Returns:
177
- `True` on success, `False` on `OSError`.
119
+ path: Absolute or ``app_dir``-relative path.
120
+ missing_ok: Ignore a missing file instead of raising
121
+ ``FileNotFoundError`` (same as ``Path.unlink``).
178
122
  """
179
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
180
- try:
181
- os.makedirs(os.path.dirname(full), exist_ok=True)
182
- with open(full, "wb") as f:
183
- f.write(data)
184
- return True
185
- except OSError:
186
- return False
123
+ FileSystem.path(path).unlink(missing_ok=missing_ok)
187
124
 
188
125
  @staticmethod
189
- def get_size(path: str) -> Optional[int]:
190
- """Return file size in bytes.
191
-
192
- Args:
193
- path: Absolute or `app_dir`-relative path.
194
-
195
- Returns:
196
- File size in bytes, or `None` if the file is missing or
197
- unreadable.
198
- """
199
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
200
- try:
201
- return os.path.getsize(full)
202
- except OSError:
203
- return None
126
+ def list_dir(path: PathLike = "") -> List[str]:
127
+ """Return the entry names in a directory (``app_dir`` by default), sorted."""
128
+ return sorted(entry.name for entry in FileSystem.path(path).iterdir())
204
129
 
205
130
  @staticmethod
206
- def ensure_dir(path: str) -> bool:
207
- """Create a directory (and any missing parents) idempotently.
208
-
209
- Args:
210
- path: Absolute or `app_dir`-relative path.
131
+ def get_size(path: PathLike) -> int:
132
+ """Return a file's size in bytes."""
133
+ return FileSystem.path(path).stat().st_size
211
134
 
212
- Returns:
213
- `True` on success or if the directory already exists.
214
- """
215
- full = path if os.path.isabs(path) else os.path.join(FileSystem.app_dir(), path)
216
- try:
217
- os.makedirs(full, exist_ok=True)
218
- return True
219
- except OSError:
220
- return False
135
+ @staticmethod
136
+ def ensure_dir(path: PathLike) -> Path:
137
+ """Create a directory (and any missing parents) if needed; returns its ``Path``."""
138
+ target = FileSystem.path(path)
139
+ target.mkdir(parents=True, exist_ok=True)
140
+ return target
221
141
 
222
142
  @staticmethod
223
143
  def join(*parts: Any) -> str:
224
- """Join path components using the OS separator.
225
-
226
- Equivalent to `os.path.join(*map(str, parts))`. Provided as a
227
- convenience so callers do not need to import `os.path`
228
- directly.
229
-
230
- Args:
231
- *parts: Path components (each coerced to `str`).
232
-
233
- Returns:
234
- The joined path string.
235
- """
144
+ """Join path components with the OS separator (``os.path.join`` over ``str(part)``)."""
236
145
  return os.path.join(*[str(p) for p in parts])
@@ -10,16 +10,15 @@ Two interfaces live here, both backed by the native ``Haptics`` module:
10
10
  milliseconds" interface for cases where you want an explicit
11
11
  duration.
12
12
 
13
- Every method is synchronous and best-effort: on a device that lacks a
14
- Taptic Engine / vibrator, or on desktop, calls are silent no-ops rather
15
- than errors.
13
+ Every method is synchronous (the OS queues the effect and returns).
14
+ Missing hardware is not an error: on a device without a Taptic Engine
15
+ or vibrator, and on desktop, the native module simply does nothing.
16
16
  """
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
20
  from typing import Any
21
21
 
22
- from .. import diagnostics
23
22
  from .registry import native_module
24
23
 
25
24
  ImpactStyle = str # "light" | "medium" | "heavy" | "soft" | "rigid"
@@ -27,14 +26,15 @@ NotificationType = str # "success" | "warning" | "error"
27
26
 
28
27
 
29
28
  def _call(method: str, **args: Any) -> None:
30
- try:
31
- native_module("Haptics").call(method, **args)
32
- except Exception:
33
- diagnostics.swallowed(f"haptics.{method}")
29
+ native_module("Haptics").call(method, **args)
34
30
 
35
31
 
36
32
  class Haptics:
37
- """Semantic haptic feedback (synchronous, best-effort)."""
33
+ """Semantic haptic feedback (synchronous).
34
+
35
+ Raises:
36
+ NativeModuleError: If the native module fails.
37
+ """
38
38
 
39
39
  @staticmethod
40
40
  def impact(style: ImpactStyle = "medium") -> None:
@@ -53,7 +53,11 @@ class Haptics:
53
53
 
54
54
 
55
55
  class Vibration:
56
- """Raw vibration control (synchronous, best-effort)."""
56
+ """Raw vibration control (synchronous).
57
+
58
+ Raises:
59
+ NativeModuleError: If the native module fails.
60
+ """
57
61
 
58
62
  @staticmethod
59
63
  def vibrate(duration_ms: int = 400) -> None:
@@ -5,9 +5,9 @@
5
5
  app can hand a URL (``https:``, ``mailto:``, ``tel:``, a custom
6
6
  scheme, ...) to the OS.
7
7
 
8
- Outbound methods are synchronous and return a ``bool`` describing
9
- whether the platform accepted the request. On desktop they return
10
- ``False``.
8
+ Outbound methods hand the URL to the OS and return at once, so they
9
+ are synchronous; the ``bool`` says whether the platform accepted the
10
+ request. On desktop they return ``False``.
11
11
 
12
12
  Inbound deep links flow the other way: declare your schemes in
13
13
  ``pythonnative.toml`` (``app.url_schemes``) and the native module
@@ -39,14 +39,15 @@ _url_listeners: List[Callable[[str], None]] = []
39
39
 
40
40
 
41
41
  def _call_bool(method: str, **args: Any) -> bool:
42
- try:
43
- return bool(native_module("Linking").call(method, **args))
44
- except Exception:
45
- return False
42
+ return bool(native_module("Linking").call(method, **args))
46
43
 
47
44
 
48
45
  class Linking:
49
- """System URL / deep-link interface (synchronous)."""
46
+ """System URL / deep-link interface (synchronous).
47
+
48
+ Raises:
49
+ NativeModuleError: If the native module fails.
50
+ """
50
51
 
51
52
  @staticmethod
52
53
  def open_url(url: str) -> bool:
@@ -2,7 +2,8 @@
2
2
 
3
3
  [`Location.get_current`][pythonnative.native_modules.location.Location.get_current]
4
4
  is a coroutine that resolves to a ``(latitude, longitude)`` tuple, or
5
- ``None`` if no recent fix is available or the user denies permission.
5
+ ``None`` if no fix is available (the user denied permission, location
6
+ services are off, or the request timed out).
6
7
  The native ``Location`` module owns the ``CLLocationManager`` /
7
8
  ``LocationManager`` session and resolves the call with
8
9
  ``{"latitude", "longitude", "accuracy", "altitude", "timestamp"}``.
@@ -29,7 +30,7 @@ from __future__ import annotations
29
30
 
30
31
  from typing import Any, Dict, Optional, Tuple
31
32
 
32
- from .registry import native_module
33
+ from .registry import NativeModuleError, native_module
33
34
 
34
35
  Coords = Tuple[float, float]
35
36
 
@@ -48,6 +49,9 @@ class Location:
48
49
  Returns:
49
50
  ``(latitude, longitude)`` if a fix was obtained, otherwise
50
51
  ``None``.
52
+
53
+ Raises:
54
+ NativeModuleError: If the native module fails.
51
55
  """
52
56
  fix = await Location.get_current_fix(**options)
53
57
  if fix is None:
@@ -62,16 +66,10 @@ class Location:
62
66
  them ``accuracy`` (meters), ``altitude`` (meters), ``speed``
63
67
  (m/s), ``heading`` (degrees), ``timestamp`` (Unix seconds).
64
68
  """
65
- try:
66
- result = await native_module("Location").call_async("get_current", **options)
67
- except Exception:
68
- return None
69
+ result = await native_module("Location").call_async("get_current", **options)
69
70
  if not isinstance(result, dict):
70
71
  return None
71
- try:
72
- fix: Dict[str, float] = {k: float(v) for k, v in result.items() if v is not None}
73
- fix["latitude"]
74
- fix["longitude"]
75
- except (KeyError, TypeError, ValueError):
76
- return None
72
+ fix: Dict[str, float] = {k: float(v) for k, v in result.items() if v is not None}
73
+ if "latitude" not in fix or "longitude" not in fix:
74
+ raise NativeModuleError("Location", "get_current", f"fix is missing coordinates: {sorted(result)}")
77
75
  return fix
@@ -41,10 +41,7 @@ class NetInfo:
41
41
  def fetch() -> NetInfoState:
42
42
  """Return a fresh snapshot of connectivity state."""
43
43
  global _last_state
44
- try:
45
- snapshot = native_module("NetInfo").call("fetch")
46
- except Exception:
47
- snapshot = None
44
+ snapshot = native_module("NetInfo").call("fetch")
48
45
  if isinstance(snapshot, dict):
49
46
  _last_state = _normalize(snapshot)
50
47
  return dict(_last_state)
@@ -39,7 +39,7 @@ from __future__ import annotations
39
39
 
40
40
  from typing import Any, Optional
41
41
 
42
- from .registry import NativeModuleError, native_module
42
+ from .registry import native_module
43
43
 
44
44
 
45
45
  class Notifications:
@@ -55,13 +55,13 @@ class Notifications:
55
55
  is shown if the user hasn't decided yet.
56
56
 
57
57
  Returns:
58
- ``True`` if granted (or no prompt is needed), ``False``
59
- otherwise.
58
+ ``True`` if granted (or no prompt is needed), ``False`` if
59
+ the user declined (always ``False`` on desktop).
60
+
61
+ Raises:
62
+ NativeModuleError: If the native module fails.
60
63
  """
61
- try:
62
- return bool(await native_module("Notifications").call_async("request_permission"))
63
- except Exception:
64
- return False
64
+ return bool(await native_module("Notifications").call_async("request_permission"))
65
65
 
66
66
  @staticmethod
67
67
  async def schedule(
@@ -86,30 +86,26 @@ class Notifications:
86
86
  ``badge``, ...). Unknown keys are ignored.
87
87
 
88
88
  Returns:
89
- ``True`` on success, ``False`` if the underlying native
90
- call failed or (on Android 13+) the user denied the
91
- notification permission.
89
+ ``True`` once scheduled, ``False`` if the user has denied
90
+ notification permission (so nothing was scheduled).
91
+
92
+ Raises:
93
+ NativeModuleError: If the native module fails.
92
94
  """
93
- try:
94
- result = await native_module("Notifications").call_async(
95
- "schedule",
96
- title=title,
97
- body=body,
98
- delay_seconds=float(delay_seconds),
99
- identifier=identifier,
100
- **options,
101
- )
102
- except Exception:
103
- return False
95
+ result = await native_module("Notifications").call_async(
96
+ "schedule",
97
+ title=title,
98
+ body=body,
99
+ delay_seconds=float(delay_seconds),
100
+ identifier=identifier,
101
+ **options,
102
+ )
104
103
  return bool(result)
105
104
 
106
105
  @staticmethod
107
106
  async def cancel(identifier: str = "default") -> None:
108
- """Cancel a pending notification by its identifier."""
109
- try:
110
- await native_module("Notifications").call_async("cancel", identifier=identifier)
111
- except Exception:
112
- pass
107
+ """Cancel a pending notification by its identifier (a no-op when none is pending)."""
108
+ await native_module("Notifications").call_async("cancel", identifier=identifier)
113
109
 
114
110
  @staticmethod
115
111
  async def get_device_token() -> Optional[str]:
@@ -126,11 +122,8 @@ class Notifications:
126
122
  remote push support (Android and desktop).
127
123
 
128
124
  Raises:
129
- RuntimeError: If APNs registration fails, with the native
130
- error description.
125
+ NativeModuleError: If APNs registration fails; ``code`` is
126
+ ``"apns"`` and the message carries the system's error.
131
127
  """
132
- try:
133
- token = await native_module("Notifications").call_async("get_device_token")
134
- except NativeModuleError as exc:
135
- raise RuntimeError(f"APNs registration failed: {exc.message}") from exc
128
+ token = await native_module("Notifications").call_async("get_device_token")
136
129
  return str(token) if token else None