pulse-framework 0.1.62__tar.gz → 0.1.63__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 (126) hide show
  1. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/PKG-INFO +1 -1
  2. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/pyproject.toml +1 -1
  3. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/__init__.py +1 -0
  4. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/dependencies.py +7 -16
  5. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/component.py +1 -1
  6. pulse_framework-0.1.63/src/pulse/requirements.py +47 -0
  7. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/imports.py +22 -1
  8. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/nodes.py +55 -12
  9. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/README.md +0 -0
  10. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/_examples.py +0 -0
  11. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/app.py +0 -0
  12. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/channel.py +0 -0
  13. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/__init__.py +0 -0
  14. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/cmd.py +0 -0
  15. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/folder_lock.py +0 -0
  16. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/helpers.py +0 -0
  17. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/logging.py +0 -0
  18. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/models.py +0 -0
  19. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/packages.py +0 -0
  20. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/processes.py +0 -0
  21. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/secrets.py +0 -0
  22. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cli/uvicorn_log_config.py +0 -0
  23. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/code_analysis.py +0 -0
  24. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/codegen/__init__.py +0 -0
  25. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/codegen/codegen.py +0 -0
  26. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/codegen/templates/__init__.py +0 -0
  27. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/codegen/templates/layout.py +0 -0
  28. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/codegen/templates/route.py +0 -0
  29. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/codegen/templates/routes_ts.py +0 -0
  30. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/codegen/utils.py +0 -0
  31. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/components/__init__.py +0 -0
  32. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/components/for_.py +0 -0
  33. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/components/if_.py +0 -0
  34. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/components/react_router.py +0 -0
  35. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/context.py +0 -0
  36. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/cookies.py +0 -0
  37. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/decorators.py +0 -0
  38. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/dom/__init__.py +0 -0
  39. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/dom/elements.py +0 -0
  40. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/dom/events.py +0 -0
  41. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/dom/props.py +0 -0
  42. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/dom/svg.py +0 -0
  43. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/dom/tags.py +0 -0
  44. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/dom/tags.pyi +0 -0
  45. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/env.py +0 -0
  46. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/form.py +0 -0
  47. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/helpers.py +0 -0
  48. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/__init__.py +0 -0
  49. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/core.py +0 -0
  50. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/effects.py +0 -0
  51. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/init.py +0 -0
  52. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/runtime.py +0 -0
  53. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/setup.py +0 -0
  54. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/stable.py +0 -0
  55. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/hooks/state.py +0 -0
  56. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/__init__.py +0 -0
  57. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/__init__.pyi +0 -0
  58. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/_types.py +0 -0
  59. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/array.py +0 -0
  60. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/console.py +0 -0
  61. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/date.py +0 -0
  62. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/document.py +0 -0
  63. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/error.py +0 -0
  64. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/json.py +0 -0
  65. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/map.py +0 -0
  66. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/math.py +0 -0
  67. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/navigator.py +0 -0
  68. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/number.py +0 -0
  69. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/obj.py +0 -0
  70. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/object.py +0 -0
  71. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/promise.py +0 -0
  72. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/pulse.py +0 -0
  73. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/react.py +0 -0
  74. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/regexp.py +0 -0
  75. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/set.py +0 -0
  76. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/string.py +0 -0
  77. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/weakmap.py +0 -0
  78. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/weakset.py +0 -0
  79. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/js/window.py +0 -0
  80. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/messages.py +0 -0
  81. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/middleware.py +0 -0
  82. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/plugin.py +0 -0
  83. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/proxy.py +0 -0
  84. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/py.typed +0 -0
  85. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/__init__.py +0 -0
  86. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/client.py +0 -0
  87. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/common.py +0 -0
  88. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/effect.py +0 -0
  89. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/infinite_query.py +0 -0
  90. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/mutation.py +0 -0
  91. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/protocol.py +0 -0
  92. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/query.py +0 -0
  93. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/queries/store.py +0 -0
  94. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/react_component.py +0 -0
  95. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/reactive.py +0 -0
  96. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/reactive_extensions.py +0 -0
  97. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/render_session.py +0 -0
  98. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/renderer.py +0 -0
  99. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/request.py +0 -0
  100. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/routing.py +0 -0
  101. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/serializer.py +0 -0
  102. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/state.py +0 -0
  103. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/test_helpers.py +0 -0
  104. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/__init__.py +0 -0
  105. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/assets.py +0 -0
  106. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/builtins.py +0 -0
  107. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/dynamic_import.py +0 -0
  108. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/emit_context.py +0 -0
  109. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/errors.py +0 -0
  110. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/function.py +0 -0
  111. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/id.py +0 -0
  112. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/js_module.py +0 -0
  113. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/modules/__init__.py +0 -0
  114. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/modules/asyncio.py +0 -0
  115. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/modules/json.py +0 -0
  116. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/modules/math.py +0 -0
  117. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
  118. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
  119. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/modules/typing.py +0 -0
  120. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/py_module.py +0 -0
  121. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/transpiler.py +0 -0
  122. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/transpiler/vdom.py +0 -0
  123. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/types/__init__.py +0 -0
  124. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/types/event_handler.py +0 -0
  125. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/user_session.py +0 -0
  126. {pulse_framework-0.1.62 → pulse_framework-0.1.63}/src/pulse/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.62
3
+ Version: 0.1.63
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: websockets>=12.0
6
6
  Requires-Dist: fastapi>=0.128.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.62"
3
+ version = "0.1.63"
4
4
  description = "Pulse - Full-stack framework for building real-time React applications in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1410,6 +1410,7 @@ from pulse.render_session import run_js as run_js
1410
1410
 
1411
1411
  # Request
1412
1412
  from pulse.request import PulseRequest as PulseRequest
1413
+ from pulse.requirements import require as require
1413
1414
  from pulse.routing import Layout as Layout
1414
1415
  from pulse.routing import Route as Route
1415
1416
  from pulse.routing import RouteInfo as RouteInfo
@@ -11,11 +11,10 @@ from pulse.cli.packages import (
11
11
  is_workspace_spec,
12
12
  load_package_json,
13
13
  parse_dependency_spec,
14
- parse_install_spec,
15
14
  resolve_versions,
16
15
  spec_satisfies,
17
16
  )
18
- from pulse.transpiler.imports import get_registered_imports
17
+ from pulse.requirements import get_requirements
19
18
 
20
19
 
21
20
  def convert_pep440_to_semver(python_version: str) -> str:
@@ -98,20 +97,12 @@ def get_required_dependencies(
98
97
  "pulse-ui-client": [pulse_version],
99
98
  }
100
99
 
101
- # New transpiler v2 imports
102
- for imp in get_registered_imports():
103
- if imp.src:
104
- try:
105
- spec = parse_install_spec(imp.src)
106
- except ValueError as exc:
107
- # We might want to be more lenient here or at least log it,
108
- # but following existing pattern of raising DependencyError
109
- raise DependencyError(str(exc)) from None
110
- if spec:
111
- name_only, ver = parse_dependency_spec(spec)
112
- constraints.setdefault(name_only, []).append(ver)
113
- if imp.version:
114
- constraints.setdefault(name_only, []).append(imp.version)
100
+ for src, version in get_requirements():
101
+ name_only, ver_in_src = parse_dependency_spec(src)
102
+ if ver_in_src:
103
+ constraints.setdefault(name_only, []).append(ver_in_src)
104
+ if version:
105
+ constraints.setdefault(name_only, []).append(version)
115
106
 
116
107
  try:
117
108
  resolved = resolve_versions(constraints)
@@ -112,7 +112,7 @@ class Component(Generic[P]):
112
112
  flattened = flatten_children(
113
113
  args, # pyright: ignore[reportArgumentType]
114
114
  parent_name=f"<{self.name}>",
115
- warn_stacklevel=4,
115
+ warn_stacklevel=None,
116
116
  )
117
117
  args = tuple(flattened) # pyright: ignore[reportAssignmentType]
118
118
 
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+
5
+ _REQUIREMENTS: list[tuple[str, str]] = []
6
+
7
+
8
+ def add_requirement(name: str, version: str) -> None:
9
+ if not name or not version:
10
+ return
11
+ _REQUIREMENTS.append((name, version))
12
+
13
+
14
+ def register_requirements(packages: Mapping[str, str]) -> None:
15
+ for name, version in packages.items():
16
+ if not name or not version:
17
+ continue
18
+ add_requirement(name, version)
19
+
20
+
21
+ def get_requirements() -> list[tuple[str, str]]:
22
+ return list(_REQUIREMENTS)
23
+
24
+
25
+ def clear_requirements() -> None:
26
+ _REQUIREMENTS.clear()
27
+
28
+
29
+ def require(packages: Mapping[str, str]) -> None:
30
+ """Register npm package version requirements for dependency syncing."""
31
+ if not isinstance(packages, Mapping):
32
+ raise TypeError("require expects a mapping of package names to versions")
33
+ if not packages:
34
+ return
35
+
36
+ normalized: dict[str, str] = {}
37
+ for name, version in packages.items():
38
+ if not isinstance(name, str) or not name.strip():
39
+ raise TypeError("require expects non-empty package names")
40
+ if not isinstance(version, str) or not version.strip():
41
+ raise TypeError(f"require expects a version string for {name!r}")
42
+ normalized[name.strip()] = version.strip()
43
+
44
+ register_requirements(normalized)
45
+
46
+
47
+ __all__ = ["require"]
@@ -14,7 +14,8 @@ from typing import (
14
14
  )
15
15
  from typing import Literal as Lit
16
16
 
17
- from pulse.cli.packages import pick_more_specific
17
+ from pulse.cli.packages import parse_dependency_spec, pick_more_specific
18
+ from pulse.requirements import add_requirement, clear_requirements
18
19
  from pulse.transpiler.assets import LocalAsset, register_local_asset
19
20
  from pulse.transpiler.errors import TranspileError
20
21
  from pulse.transpiler.id import next_id
@@ -131,6 +132,14 @@ _ImportKey: TypeAlias = tuple[str, str, str, bool]
131
132
  _IMPORT_REGISTRY: dict[_ImportKey, "Import"] = {}
132
133
 
133
134
 
135
+ def _is_alias_path(path: str) -> bool:
136
+ return path.startswith("@/") or path.startswith("~/")
137
+
138
+
139
+ def _is_url(path: str) -> bool:
140
+ return path.startswith("http://") or path.startswith("https://")
141
+
142
+
134
143
  def get_registered_imports() -> list["Import"]:
135
144
  """Get all registered imports."""
136
145
  return list(_IMPORT_REGISTRY.values())
@@ -139,6 +148,7 @@ def get_registered_imports() -> list["Import"]:
139
148
  def clear_import_registry() -> None:
140
149
  """Clear the import registry."""
141
150
  _IMPORT_REGISTRY.clear()
151
+ clear_requirements()
142
152
 
143
153
 
144
154
  @dataclass(slots=True, init=False)
@@ -240,6 +250,17 @@ class Import(Expr):
240
250
  asset = register_local_asset(resolved)
241
251
  import_src = str(resolved)
242
252
 
253
+ if (
254
+ not is_local_path(import_src)
255
+ and not _is_alias_path(import_src)
256
+ and not _is_url(import_src)
257
+ ):
258
+ name_only, ver_in_src = parse_dependency_spec(import_src)
259
+ if ver_in_src:
260
+ add_requirement(name_only, ver_in_src)
261
+ if version:
262
+ add_requirement(name_only, version)
263
+
243
264
  self.name = name
244
265
  self.src = import_src
245
266
  self.kind = kind
@@ -7,7 +7,7 @@ import warnings
7
7
  from abc import ABC, abstractmethod
8
8
  from collections.abc import Callable, Iterable, Sequence
9
9
  from dataclasses import dataclass, field
10
- from inspect import isfunction, signature
10
+ from inspect import currentframe, isfunction, signature
11
11
  from typing import (
12
12
  TYPE_CHECKING,
13
13
  Any,
@@ -540,15 +540,13 @@ class Element(Expr):
540
540
  self.children = None
541
541
  else:
542
542
  if isinstance(tag, str):
543
- parent_name = tag[2:] if tag.startswith("$$") else tag
543
+ parent_name: str | Expr = tag[2:] if tag.startswith("$$") else tag
544
544
  else:
545
- tag_out: list[str] = []
546
- tag.emit(tag_out)
547
- parent_name = "".join(tag_out)
545
+ parent_name = tag
548
546
  self.children = flatten_children(
549
547
  children,
550
548
  parent_name=parent_name,
551
- warn_stacklevel=5,
549
+ warn_stacklevel=None,
552
550
  )
553
551
  self.key = key
554
552
 
@@ -787,7 +785,7 @@ class PulseNode:
787
785
  flat = flatten_children(
788
786
  children_arg,
789
787
  parent_name=parent_name,
790
- warn_stacklevel=5,
788
+ warn_stacklevel=None,
791
789
  )
792
790
  return PulseNode(
793
791
  fn=self.fn,
@@ -804,8 +802,8 @@ class PulseNode:
804
802
  def flatten_children(
805
803
  children: Sequence[Node | Iterable[Node]],
806
804
  *,
807
- parent_name: str,
808
- warn_stacklevel: int = 5,
805
+ parent_name: str | Expr,
806
+ warn_stacklevel: int | None = None,
809
807
  ) -> list[Node]:
810
808
  if env.pulse_env == "dev":
811
809
  return _flatten_children_dev(
@@ -835,13 +833,14 @@ def _flatten_children_prod(children: Sequence[Node | Iterable[Node]]) -> list[No
835
833
  def _flatten_children_dev(
836
834
  children: Sequence[Node | Iterable[Node]],
837
835
  *,
838
- parent_name: str,
839
- warn_stacklevel: int = 5,
836
+ parent_name: str | Expr,
837
+ warn_stacklevel: int | None = None,
840
838
  ) -> list[Node]:
841
839
  flat: list[Node] = []
842
840
  seen_keys: set[str] = set()
843
841
 
844
842
  def visit(item: Node | Iterable[Node]) -> None:
843
+ nonlocal warn_stacklevel
845
844
  if isinstance(item, dict):
846
845
  raise TypeError("Dict is not a valid child")
847
846
  if isinstance(item, Iterable) and not isinstance(item, str):
@@ -853,6 +852,32 @@ def _flatten_children_dev(
853
852
  missing_key = True
854
853
  visit(sub) # type: ignore[arg-type]
855
854
  if missing_key:
855
+ if warn_stacklevel is None:
856
+ stacklevel = 1
857
+ frame = currentframe()
858
+ if frame is not None:
859
+ frame = frame.f_back
860
+ internal_prefixes = (
861
+ "pulse",
862
+ "pulse_mantine",
863
+ "pulse_ag_grid",
864
+ "pulse_recharts",
865
+ "pulse_lucide",
866
+ "pulse_msal",
867
+ "pulse_aws",
868
+ )
869
+ while frame is not None:
870
+ module = frame.f_globals.get("__name__", "")
871
+ if module and not any(
872
+ module == prefix or module.startswith(f"{prefix}.")
873
+ for prefix in internal_prefixes
874
+ ):
875
+ break
876
+ stacklevel += 1
877
+ frame = frame.f_back
878
+ if frame is not None:
879
+ stacklevel += 1
880
+ warn_stacklevel = stacklevel
856
881
  clean_name = clean_element_name(parent_name)
857
882
  warnings.warn(
858
883
  (
@@ -888,7 +913,25 @@ def _flatten_children_dev(
888
913
  return flat
889
914
 
890
915
 
891
- def clean_element_name(parent_name: str) -> str:
916
+ def clean_element_name(parent_name: str | Expr) -> str:
917
+ def expr_name(expr: Expr) -> str:
918
+ while isinstance(expr, ExprWrapper):
919
+ expr = expr.expr
920
+ if isinstance(expr, Member):
921
+ base = expr_name(expr.obj)
922
+ return f"{base}.{expr.prop}" if base else expr.prop
923
+ if isinstance(expr, Identifier):
924
+ return expr.name
925
+ if expr.__class__.__name__ == "Import":
926
+ name = getattr(expr, "name", None)
927
+ if isinstance(name, str) and name:
928
+ return name
929
+ out: list[str] = []
930
+ expr.emit(out)
931
+ return "".join(out)
932
+
933
+ if isinstance(parent_name, Expr):
934
+ parent_name = expr_name(parent_name)
892
935
  if parent_name.startswith("<") and parent_name.endswith(">"):
893
936
  return parent_name
894
937
  return f"<{parent_name}>"