Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoSign in
Socket

wwwpy

Package Overview
Dependencies
Maintainers
1
Versions
115
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

wwwpy - pypi Package Compare versions

Comparing version
0.1.85
to
0.1.86
+41
src/wwwpy/common/attr_lib.py
import ast
import inspect
from collections.abc import Callable
class _AttrVisitor(ast.NodeVisitor):
def __init__(self):
self.attr = None
def visit_Lambda(self, node):
# Only support simple attribute access: lambda x: x.foo
if isinstance(node.body, ast.Attribute):
self.attr = node.body.attr
def visit_FunctionDef(self, node):
# Only support simple attribute access: def foo(x): return x.bar
if len(node.body) == 1 and isinstance(node.body[0], ast.Return):
ret = node.body[0].value
if isinstance(ret, ast.Attribute):
self.attr = ret.attr
def attr_name(block: Callable) -> str:
src = inspect.getsource(block).strip()
visitor = _AttrVisitor()
visitor.visit(ast.parse(src))
if visitor.attr is None:
raise ValueError("Callable must be of the form: lambda x: x.foo or def foo(x): return x.bar")
return visitor.attr
def test_attr_name_with_lambda():
result = attr_name(lambda x: x.foo)
assert result == "foo"
def test_attr_name_with_function():
def foo(x):
return x.bar
result = attr_name(foo)
assert result == "bar"
from pathlib import Path
_parent = Path(__file__).parent
def _svg(filename: str) -> Path: return _parent / filename
# you can control-click the icon filename to open it (at least in PyCharm)
class AllIcons:
services_dark = _svg("jb/services_dark.svg")
todo_20x20_dark = _svg("jb/todo@20x20_dark.svg")
console_dark = _svg("jb/console_dark.svg")
pythonPackages_dark = _svg("jb/pythonPackages_dark.svg")
python_stroke = _svg("jb/python_stroke.svg")
toolWindowComponents_dark = _svg("jb/toolWindowComponents_dark.svg")
properties_dark = _svg("jb/properties_dark.svg")
project_20x20_dark = _svg("jb/project@20x20_dark.svg")
structure_20x20_dark = _svg("jb/structure@20x20_dark.svg")
events = _svg("events.svg")
toolWindowComponents_20x20_dark = _svg("jb/toolWindowComponents@20x20_dark.svg")
@classmethod
def all_icons(cls) -> tuple[Path, ...]:
return _all_icons
_all_icons = tuple(
value for name, value in vars(AllIcons).items()
if isinstance(value, Path) and not name.startswith('_')
)
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill="none" stroke="#CED0D6" stroke-width="2"/>
<path d="M13 5 L8 13 H12 L11 19 L16 11 H12 L13 5 Z" fill="#CED0D6"/>
</svg>
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.12642 5.16655C4.32168 4.97129 4.63827 4.97129 4.83353 5.16655L7.16698 7.5L4.83353 9.83345C4.63827 10.0287 4.32168 10.0287 4.12642 9.83345C3.93116 9.63819 3.93116 9.32161 4.12642 9.12635L5.75277 7.5L4.12642 5.87365C3.93116 5.67839 3.93116 5.36181 4.12642 5.16655Z"
fill="#CED0D6"/>
<path d="M7.5 10C7.22386 10 7 10.2239 7 10.5C7 10.7761 7.22386 11 7.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10L7.5 10Z"
fill="#CED0D6"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M3 2C1.89543 2 1 2.89543 1 4V12C1 13.1046 1.89543 14 3 14H13C14.1046 14 15 13.1046 15 12V4C15 2.89543 14.1046 2 13 2H3ZM13 3H3C2.44772 3 2 3.44772 2 4V12C2 12.5523 2.44772 13 3 13H13C13.5523 13 14 12.5523 14 12V4C14 3.44772 13.5523 3 13 3Z"
fill="#CED0D6"/>
</svg>
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5199 5.57617L10.7285 5.75H11H17C17.6904 5.75 18.25 6.30964 18.25 7V15.1667C18.25 16.0671 17.553 16.75 16.75 16.75H3.25C2.44705 16.75 1.75 16.0671 1.75 15.1667V4.83333C1.75 3.93294 2.44705 3.25 3.25 3.25H7.63795C7.69643 3.25 7.75307 3.2705 7.798 3.30794L10.5199 5.57617Z"
stroke="#CED0D6" stroke-width="1.5"/>
</svg>
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 3.5h7m-7 4h2m-2 4h6m3-8h2m-7 4h7m-3 4h3" stroke="#CED0D6" stroke-linecap="round"/>
<circle cx="10.5" cy="3.5" r="1" stroke="#CED0D6"/>
<circle cx="5.5" cy="7.5" r="1" stroke="#CED0D6"/>
<circle cx="9.5" cy="11.5" r="1" stroke="#CED0D6"/>
</svg>
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M6.48983 1.13677C7.02636 1.04476 7.57004 0.999004 8.11466 1.00002L8.11854 1.00004C8.69493 1.00471 9.27001 1.05462 9.83831 1.14927C10.3846 1.20957 10.893 1.45404 11.277 1.8415C11.6592 2.22707 11.9477 2.79308 11.9477 3.34128V6.28964C11.9478 6.86298 11.7202 7.41385 11.3133 7.82436C10.9063 8.23491 10.3524 8.47274 9.76965 8.48707L9.75441 8.48726H6.59961C6.2432 8.49379 5.90273 8.63409 5.64798 8.87952C5.3946 9.12364 5.24517 9.45331 5.22975 9.8016V11.3C5.22975 11.631 4.95698 11.8993 4.6205 11.8993H3.51574C2.88773 11.8993 2.3666 11.6627 1.97683 11.2638C1.60371 10.882 1.37487 10.3779 1.24477 9.85376C0.919717 8.71373 0.918415 7.50798 1.24086 6.36742C1.35923 5.78344 1.68245 5.25856 2.1545 4.88482C2.63144 4.50721 3.22913 4.30857 3.84095 4.32413H4.4093V3.3253C4.4093 2.85137 4.46916 2.32352 4.8446 1.89321C5.21181 1.47233 5.7798 1.26027 6.48837 1.13702L6.48983 1.13677ZM8.11043 2.19861C7.63761 2.19784 7.16563 2.2376 6.69983 2.31743C6.07673 2.4259 5.85691 2.57335 5.76924 2.67382C5.68978 2.7649 5.62779 2.92025 5.62779 3.3253C5.62779 3.72444 5.62779 4.12358 5.62779 4.52273C5.62779 5.07501 5.18007 5.52273 4.62779 5.52273H3.83255C3.82654 5.52273 3.82054 5.52264 3.81454 5.52247C3.48954 5.51301 3.17179 5.618 2.91851 5.81854C2.63197 6.0454 2.51677 6.33318 2.42054 6.66843C2.15114 7.60694 2.15114 8.60048 2.42054 9.53899C2.51264 9.85982 2.61418 10.1865 2.85527 10.4332C3.01991 10.6017 3.22582 10.7007 3.51574 10.7007H4.01126V9.78974C4.01126 9.78248 4.01139 9.77523 4.01166 9.76797C4.036 9.1091 4.31654 8.48455 4.79538 8.02322C5.27422 7.56189 5.91482 7.29898 6.585 7.28873L6.59446 7.28859L9.74591 7.28866C10.0083 7.2805 10.2574 7.17265 10.4408 6.98761C10.6258 6.801 10.7293 6.55065 10.7292 6.29002V3.36692C10.704 3.10789 10.5896 2.86485 10.4046 2.67824C10.194 2.46572 9.94035 2.38246 9.65164 2.33386C9.14243 2.24813 8.62702 2.2029 8.11043 2.19861Z"
fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M10.7292 4.57159C10.7292 4.24061 11.0019 3.97229 11.3384 3.97229H12.565C13.2089 3.97229 13.7126 4.2299 14.0744 4.6449C14.4106 5.0305 14.604 5.5269 14.7275 6.00692C15.0925 7.15942 15.0907 8.39428 14.7221 9.54595L14.7208 9.54984C14.5615 10.036 14.3678 10.5376 14.0436 10.9147C13.6839 11.3329 13.1965 11.5634 12.565 11.5634H11.5902V12.5622C11.5902 13.2231 11.2876 13.7249 10.8697 14.0771C10.473 14.4114 9.97593 14.6111 9.53055 14.7347C8.38602 15.0892 7.15819 15.0884 6.01405 14.7322C5.13141 14.4621 4.01123 13.8361 4.01123 12.5622V11.2438C4.01123 10.9128 4.284 10.6445 4.62048 10.6445C4.95695 10.6445 5.22972 10.9128 5.22972 11.2438V12.5622C5.22972 12.9817 5.58664 13.3465 6.37715 13.5881L6.3804 13.5891C7.2888 13.8722 8.26394 13.8722 9.17234 13.5891C9.49224 13.4894 9.81727 13.3857 10.0769 13.1669C10.2682 13.0057 10.3718 12.8204 10.3718 12.5622C10.3718 12.3041 10.3718 11.8157 10.3718 11.3647C10.3718 10.8125 10.8195 10.3648 11.3718 10.3648H12.565C12.8515 10.3648 12.9937 10.2797 13.1133 10.1406C13.2681 9.96054 13.4032 9.66363 13.5604 9.18415C13.8553 8.26157 13.8551 7.27218 13.5598 6.34969C13.4565 6.0269 13.3799 5.68945 13.1495 5.4252C13.0097 5.26486 12.8391 5.17089 12.565 5.17089H11.3384C11.0019 5.17089 10.7292 4.90257 10.7292 4.57159Z"
fill="white"/>
<path d="M7.6514 3.59699C7.80442 3.59511 7.95456 3.63803 8.08273 3.72029C8.2109 3.80255 8.31131 3.92043 8.37121 4.05897C8.4311 4.1975 8.44776 4.35042 8.41908 4.4983C8.3904 4.64617 8.31768 4.78232 8.21015 4.88943C8.10261 4.99655 7.96513 5.06979 7.81518 5.09986C7.66522 5.12993 7.50957 5.11546 7.36799 5.05829C7.22642 5.00113 7.10532 4.90385 7.02009 4.77881C6.93486 4.65378 6.88934 4.50664 6.88933 4.3561C6.88805 4.25687 6.90682 4.15837 6.94455 4.06632C6.98227 3.97427 7.0382 3.89051 7.1091 3.81989C7.17999 3.74928 7.26443 3.69321 7.35752 3.65495C7.45061 3.61668 7.5505 3.59698 7.6514 3.59699Z"
fill="white"/>
<path d="M8.32857 11.0484C8.53325 11.0484 8.72953 11.1284 8.87426 11.2707C9.01898 11.4131 9.10029 11.6062 9.10029 11.8075C9.10029 12.0088 9.01898 12.2019 8.87426 12.3443C8.72953 12.4866 8.53325 12.5666 8.32857 12.5666C8.1239 12.5666 7.92762 12.4866 7.78289 12.3443C7.63817 12.2019 7.55686 12.0088 7.55686 11.8075C7.55686 11.6062 7.63817 11.4131 7.78289 11.2707C7.92762 11.1284 8.1239 11.0484 8.32857 11.0484Z"
fill="white"/>
</svg>
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M6.5 0.375L12.625 3.77778L6.5 7.18056L0.375 3.77778L6.5 0.375ZM11.4 5.81944L12.625 6.5L6.5 9.90278L0.375 6.5L1.6 5.81944L6.5 8.54167L11.4 5.81944ZM11.4 8.54167L12.625 9.22222L6.5 12.625L0.375 9.22222L1.6 8.54167L6.5 11.2639L11.4 8.54167Z"
fill="#AFB1B3"/>
</svg>
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.2117 7.50028C15.3901 7.80954 15.3901 8.19046 15.2117 8.49972L12.0386 13.9997C11.86 14.3093 11.5298 14.5 11.1724 14.5L4.82756 14.5C4.47018 14.5 4.13997 14.3093 3.96138 13.9997L0.788301 8.49972C0.609882 8.19046 0.609882 7.80954 0.788301 7.50028L3.96138 2.00028C4.13997 1.69072 4.47018 1.5 4.82756 1.5L11.1724 1.5C11.5298 1.5 11.86 1.69072 12.0386 2.00028L15.2117 7.50028Z"
stroke="#CED0D6"/>
<path d="M10.5 8.43301L6.75 10.5981C6.41667 10.7905 6 10.55 6 10.1651L6 5.83493C6 5.45004 6.41667 5.20947 6.75 5.40192L10.5 7.56699C10.8333 7.75944 10.8333 8.24056 10.5 8.43301Z"
stroke="#CED0D6"/>
</svg>
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M2.75 1C3.16421 1 3.5 1.33579 3.5 1.75V4H6V3C6 1.89543 6.89543 1 8 1H16C17.1046 1 18 1.89543 18 3V7C18 8.10457 17.1046 9 16 9H8C6.89543 9 6 8.10457 6 7V5.5H3.5V14H6V13C6 11.8954 6.89543 11 8 11H16C17.1046 11 18 11.8954 18 13V17C18 18.1046 17.1046 19 16 19H8C6.89543 19 6 18.1046 6 17V15.5H3.5V18.25C3.5 18.6642 3.16421 19 2.75 19C2.33579 19 2 18.6642 2 18.25V1.75C2 1.33579 2.33579 1 2.75 1ZM16 12.5H8C7.72386 12.5 7.5 12.7239 7.5 13V17C7.5 17.2761 7.72386 17.5 8 17.5H16C16.2761 17.5 16.5 17.2761 16.5 17V13C16.5 12.7239 16.2761 12.5 16 12.5ZM8 2.5H16C16.2761 2.5 16.5 2.72386 16.5 3V7C16.5 7.27614 16.2761 7.5 16 7.5H8C7.72386 7.5 7.5 7.27614 7.5 7V3C7.5 2.72386 7.72386 2.5 8 2.5Z"
fill="#CED0D6"/>
</svg>
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="3.1" cy="4.69998" r="1.1" fill="#CED0D6"/>
<path d="M5.5 4.70001C5.5 4.2858 5.83579 3.95001 6.25 3.95001H17.25C17.6642 3.95001 18 4.2858 18 4.70001V4.70001C18 5.11423 17.6642 5.45001 17.25 5.45001H6.25C5.83579 5.45001 5.5 5.11423 5.5 4.70001V4.70001Z"
fill="#CED0D6"/>
<circle cx="3.1" cy="10" r="1.1" fill="#CED0D6"/>
<path d="M5.5 10C5.5 9.58579 5.83579 9.25 6.25 9.25H17.25C17.6642 9.25 18 9.58579 18 10V10C18 10.4142 17.6642 10.75 17.25 10.75H6.25C5.83579 10.75 5.5 10.4142 5.5 10V10Z"
fill="#CED0D6"/>
<circle cx="3.1" cy="15.3" r="1.1" fill="#CED0D6"/>
<path d="M5.5 15.3C5.5 14.8858 5.83579 14.55 6.25 14.55H17.25C17.6642 14.55 18 14.8858 18 15.3V15.3C18 15.7142 17.6642 16.05 17.25 16.05H6.25C5.83579 16.05 5.5 15.7142 5.5 15.3V15.3Z"
fill="#CED0D6"/>
</svg>
<!-- Copyright © 2000–2024 JetBrains s.r.o. -->
<svg width="13" height="13" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
<path fill="#afb1b3" d="M6 6H1V1h5zm0 1H1v5h5zm6 0H7v5h5zM9.5.5l-3 3 3 3 3-3z"/>
</svg>
<!-- Copyright © 2000–2024 JetBrains s.r.o. -->
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M10 18.15v-15c0-.69-.56-1.25-1.25-1.25H3.12c-.69 0-1.25.56-1.25 1.25V16.9c0 .69.56 1.25 1.25 1.25h13.75c.69 0 1.25-.56 1.25-1.25v-5.62c0-.69-.56-1.25-1.25-1.25H1.88"/>
<path fill="none" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M18.96 4.58c.19.19.29.45.29.7s-.1.51-.29.7l-3.51 3.51a.984.984 0 0 1-1.4 0l-3.51-3.51a.984.984 0 0 1 0-1.4l3.51-3.51c.97-.97 1.69.28 4.91 3.51"/>
</svg>
from __future__ import annotations
import logging
from functools import cached_property
from wwwpy.common.collectionlib import ObservableList
logger = logging.getLogger(__name__)
_node_id = 0
class NodeList(ObservableList):
def __init__(self, *args):
super().__init__(*args)
self._parent = None
def _item_added(self, item, index):
if isinstance(item, Node):
item._parent = self
else:
logger.warning(f'Expected Node, got {type(item)}')
def _item_removed(self, item, index):
if isinstance(item, Node):
item._parent = None
else:
logger.warning(f'Expected Node, got {type(item)}')
@property
def children(self) -> list[Node]:
return self
def selected_nodes(self) -> set[Node]:
uf = [n for n in self.children if n.selected]
s = set(uf)
for ch in self.children:
s.update(ch.selected_nodes())
return s
def deselect_all(self):
for ch in self.children:
ch.selected = False
for ch in self.children:
ch.deselect_all()
def root(self) -> NodeList:
if self._parent is None:
return self
return self._parent.root()
class Node(NodeList):
def __init__(self):
super().__init__()
self._selected = False
@property
def selected(self) -> bool:
return self._selected
@selected.setter
def selected(self, value: bool):
if value == self._selected:
return
if value is True:
self.root().deselect_all()
self._selected = value
def _perform_click(self):
self.selected = True
def _perform_toggle(self):
raise NotImplementedError
def __repr__(self):
return f'Node(id={self.node_id})'
@cached_property
def node_id(self) -> int:
global _node_id
_node_id += 1
return _node_id
def __eq__(self, other):
return self is other
def __hash__(self):
return id(self)
class Tree(NodeList):
pass
import wwwpy.remote.component as wpc
from wwwpy.remote import dict_to_js
from wwwpy.remote.designer.ui.tree.custom_tree import TreeElement, ItemPresentation, CustomTree
# Helper to build TreeElement hierarchy from JSON
def createTreeFromJSON(json_node, parent=None):
text = json_node.get('text', 'Unnamed')
icon = json_node.get('icon', None)
bg = json_node.get('backgroundColor', None)
pres = ItemPresentation(text, icon, bg)
element = None
def loader():
return [createTreeFromJSON(child, element) for child in json_node.get('children', [])]
element = TreeElement(pres, loader, parent)
return element
# Demo component that showcases the CustomTree usage
class CustomTreeDemo(wpc.Component, tag_name='custom-tree-demo'):
fileTree: CustomTree = wpc.element()
orgTree: CustomTree = wpc.element()
def init_component(self):
# attach shadow DOM and template
self.element.attachShadow(dict_to_js({'mode': 'open'}))
self.element.shadowRoot.innerHTML = """
<style>
:host { display: block; }
.tree-container { background: #161b22; border-radius: 8px; padding: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); max-width: 600px; border: 1px solid #30363d; margin-bottom: 30px; }
.demo-title { color: #f0f6fc; margin-bottom: 10px; font-size: 1.2em; }
</style>
<div class="demo-section">
<h2 class="demo-title">File System Tree Example</h2>
<div class="tree-container">
<custom-tree data-name="fileTree"></custom-tree>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Organization Tree Example</h2>
<div class="tree-container">
<custom-tree data-name="orgTree"></custom-tree>
</div>
</div>
"""
async def after_init_component(self):
# JSON for file system tree
fileSystemJSON = {
'text': 'Project Root', 'icon': '📁',
'children': [
{'text': 'src', 'icon': '📁', 'children': [
{'text': 'components', 'icon': '📁', 'children': [
{'text': 'Button.js', 'icon': '📄'},
{'text': 'Modal.js', 'icon': '📄'},
{'text': 'Tree.js', 'icon': '📄', 'backgroundColor': '#3d2817'}
]},
{'text': 'utils', 'icon': '📁', 'backgroundColor': '#2d4a22', 'children': [
{'text': 'helpers.js', 'icon': '📄'},
{'text': 'constants.js', 'icon': '📄'}
]},
{'text': 'index.js', 'icon': '📄', 'backgroundColor': '#1f2937'}
]},
{'text': 'public', 'icon': '📁', 'children': [
{'text': 'index.html', 'icon': '🌐'},
{'text': 'favicon.ico', 'icon': '🖼️', 'backgroundColor': '#4c1d95'}
]},
{'text': 'package.json', 'icon': '📦'},
{'text': 'README.md', 'icon': '📝'}
]
}
# JSON for organization tree
organizationJSON = {
'text': 'TechCorp Inc.', 'icon': '🏢', 'backgroundColor': '#1e293b',
'children': [
{'text': 'Engineering', 'icon': '⚙️', 'backgroundColor': '#1f2937', 'children': [
{'text': 'Frontend Team', 'icon': '💻', 'children': [
{'text': 'Alice Johnson - Lead', 'icon': '👤', 'backgroundColor': '#7c2d12'},
{'text': 'Bob Smith - Developer', 'icon': '👤'},
{'text': 'Carol Williams - Developer', 'icon': '👤'}
]},
{'text': 'Backend Team', 'icon': '🔧', 'children': [
{'text': 'David Brown - Lead', 'icon': '👤', 'backgroundColor': '#7c2d12'},
{'text': 'Eva Davis - Developer', 'icon': '👤'}
]}
]},
{'text': 'Product', 'icon': '📊', 'backgroundColor': '#2d1b69', 'children': [
{'text': 'Sarah Wilson - Manager', 'icon': '👤', 'backgroundColor': '#7c2d12'},
{'text': 'Mike Chen - Designer', 'icon': '👤'}
]},
{'text': 'Sales', 'icon': '💼', 'backgroundColor': '#14532d', 'children': [
{'text': 'Tom Anderson - Director', 'icon': '👤', 'backgroundColor': '#7c2d12'},
{'text': 'Lisa Garcia - Rep', 'icon': '👤'}
]}
]
}
# build trees
fileRoot = createTreeFromJSON(fileSystemJSON)
orgRoot = createTreeFromJSON(organizationJSON)
# render
self.fileTree.setRoot(fileRoot)
self.orgTree.setRoot(orgRoot)
# add selection listeners
# def on_file_select(e): js.console.log('File selected:', e.detail.treeElement.presentation.getPresentableText())
# self.fileTree.addEventListener('nodeSelect', create_proxy(on_file_select))
# def on_org_select(e): js.console.log('Person/Department selected:', e.detail.treeElement.presentation.getPresentableText())
# self.orgTree.addEventListener('nodeSelect', create_proxy(on_org_select))
from __future__ import annotations
import js
from pyodide.ffi import create_proxy
import wwwpy.remote.component as wpc
from wwwpy.remote import dict_to_js
class ItemPresentation:
def __init__(self, text, icon=None, backgroundColor=None):
self.text = text if not icon else f"{icon} {text}"
self.icon = icon
self.backgroundColor = backgroundColor
# Tree node model with lazy child loading and rendering
class TreeElement:
def __init__(self, presentation: ItemPresentation, child_loader, parent=None):
self.presentation = presentation
self.child_loader = child_loader
self.parent = parent
self.nodeDiv = None
self.contentDiv = None
self.children = None
@property
def level(self):
return self.parent.level + 1 if self.parent else 0
def getChildren(self):
if self.children is None:
self.children = self.child_loader()
return self.children
def hasChildren(self):
return len(self.getChildren()) > 0
def getNodeFragment(self, default_template):
frag = default_template.content.cloneNode(True)
self.nodeDiv = frag.querySelector('[data-id="node"]')
self.contentDiv = frag.querySelector('[data-id="content"]')
return frag
def toggle(self):
childrenDiv = self.nodeDiv.querySelector('.tree-node-children')
toggleBtn = self.nodeDiv.querySelector('.tree-node-toggle')
expanded = childrenDiv.classList.contains('expanded')
if expanded:
childrenDiv.classList.replace('expanded', 'collapsed')
toggleBtn.classList.remove('expanded')
else:
childrenDiv.classList.replace('collapsed', 'expanded')
toggleBtn.classList.add('expanded')
def render_fragment(self, default_template: js.HTMLTemplateElement, tree_instance: CustomTree):
frag = self.getNodeFragment(default_template)
self.nodeDiv.dataset.level = self.level
hasKids = self.hasChildren()
btn = frag.querySelector('[data-id="toggle-btn"]')
btn.classList.toggle('leaf', not hasKids)
if hasKids:
childrenDiv = frag.querySelector('[data-id="children-container"]')
childrenDiv.className = 'tree-node-children collapsed'
def on_toggle(e):
e.stopPropagation()
if not childrenDiv.hasChildNodes():
for c in self.getChildren():
fr = c.render_fragment(default_template, tree_instance)
childrenDiv.appendChild(fr)
self.toggle()
btn.addEventListener('click', create_proxy(on_toggle))
textSpan = frag.querySelector('[data-id="text"]')
textSpan.textContent = self.presentation.text
self.contentDiv.style.paddingLeft = f"{self.level * 20 + 8}px"
bg = self.presentation.backgroundColor
if bg:
self.contentDiv.style.backgroundColor = bg
def on_select(ev):
tree_instance.selectNode(self.contentDiv, self)
self.contentDiv.addEventListener('click', create_proxy(on_select))
return frag
# Web component for rendering a tree
class CustomTree(wpc.Component, tag_name='custom-tree'):
_tree_container: js.HTMLDivElement = wpc.element()
_default_template: js.HTMLTemplateElement = wpc.element()
def init_component(self):
# attach shadow and template
self.element.attachShadow(dict_to_js({'mode': 'open'}))
self.element.shadowRoot.innerHTML = """
<style id="tree-style">
:host { display: block; }
.tree-node { margin: 0; user-select: none; }
.tree-node-content { display: flex; align-items: center; padding: 2px; font-size: 12px; width: 100%; box-sizing: border-box; border-radius: 4px; }
.tree-node-content.selected { background-color: #1f6feb !important; color: #ffffff; }
.tree-node-toggle { width: 16px; height: 16px; margin-right: 4px; border: none; background: none; font-size: 12px; display: flex; align-items: center; justify-content: center; color: #8b949e; }
.tree-node-toggle .toggle-icon { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; }
.tree-node-toggle.expanded .toggle-icon { transform: rotate(90deg); }
.tree-node-toggle.leaf { visibility: hidden; }
.tree-node-text { flex: 1; padding: 2px 4px; }
.tree-node-children { margin-left: 0; overflow: auto; }
.tree-node-children.collapsed { max-height: 0; }
.tree-node-children.expanded { max-height: 1000px; }
</style>
<template data-name="_default_template">
<div class="tree-node" data-id="node" data-level="">
<div class="tree-node-content" data-id="content">
<button class="tree-node-toggle" data-id="toggle-btn">
<span class="toggle-icon" data-id="toggle-icon">
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 11.5L9.5 8L6 4.5" stroke="#B4B8BF" stroke-linecap="round"/>
</svg>
</span>
</button>
<span class="tree-node-text" data-id="text"></span>
</div>
<div class="tree-node-children" data-id="children-container"></div>
</div>
</template>
<div data-name="_tree_container"></div>
"""
self.root = None
self.selectedNode = None
def setRoot(self, root: TreeElement):
self.root = root
self._tree_container.innerHTML = ''
if root:
self._tree_container.append(root.render_fragment(self._default_template, self, ))
def selectNode(self, contentDiv, treeElement):
if self.selectedNode:
self.selectedNode.classList.remove('selected')
contentDiv.classList.add('selected')
self.selectedNode = contentDiv
self.element.dispatchEvent(js.CustomEvent.new(
'nodeSelect',
dict_to_js({'detail': {'treeElement': treeElement}})
))
+6
-3
Metadata-Version: 2.4
Name: wwwpy
Version: 0.1.85
Version: 0.1.86
Summary: Build Powerful Web Applications: Simple, Scalable, and Fully Customizable

@@ -232,6 +232,7 @@ Author-email: Simone Giacomelli <simone.giacomelli@gmail.com>

Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: playwright; extra == "test"
Requires-Dist: pytest-playwright; extra == "test"
Requires-Dist: pytest-xvirt; extra == "test"
Requires-Dist: libcst==1.4.0; extra == "test"
Requires-Dist: libcst==1.6.0; extra == "test"
Requires-Dist: rope==1.13.0; extra == "test"

@@ -246,4 +247,6 @@ Provides-Extra: dev

Requires-Dist: build; extra == "pypi"
Provides-Extra: stubs
Requires-Dist: pyodide-stubs; extra == "stubs"
Provides-Extra: all
Requires-Dist: wwwpy[dev,pypi,test]; extra == "all"
Requires-Dist: wwwpy[dev,pypi,stubs,test]; extra == "all"
Dynamic: license-file

@@ -250,0 +253,0 @@

# https://packaging.python.org/en/latest/tutorials/packaging-projects/
[project]
name = "wwwpy"
version = "0.1.85"
version = "0.1.86"

@@ -47,12 +47,16 @@ # todo

[tool.setuptools.package-data]
"wwwpy" = ["**/*.json", "**/*.txt"]
"wwwpy" = ["**/*.json", "**/*.txt", "**/*.svg"]
[project.optional-dependencies]
# libcst==1.4.0 is the version available in pyodide 0.27.2
test = ["tox", "pytest", "playwright", "pytest-playwright", "pytest-xvirt", "libcst==1.4.0", "rope==1.13.0"]
# libcst==1.6.0 is the version available in pyodide 0.28.0
test = ["tox", "pytest", "pytest-asyncio", "playwright", "pytest-playwright", "pytest-xvirt", "libcst==1.6.0", "rope==1.13.0"]
dev = ["webtypy", "playwright", "setuptools", "pytest-asyncio"] # setuptools is needed from PyCharm
pypi = ["twine", "build"]
# stubs = ["pyodide-stubs @ file://./pyodide-stubs"]
#stubs = ["pyodide-stubs @ file:///${PWD}/pyodide-stubs"]
stubs = ["pyodide-stubs"]
# pip install -e ".[all]" or pip install -e ".[test,dev]"
all = ["wwwpy[test,dev,pypi]"]
#all = ["wwwpy[test,dev,pypi]"]
all = ["wwwpy[test,dev,pypi,stubs]"]

@@ -68,2 +72,8 @@

[project.scripts]
wwwpy = "wwwpy.server.__main__:main"
wwwpy = "wwwpy.server.__main__:main"
#[tool.mypy]
#mypy_path = ["stubs"]
[tool.uv.sources]
pyodide-stubs = { path = "pyodide-stubs" }
Metadata-Version: 2.4
Name: wwwpy
Version: 0.1.85
Version: 0.1.86
Summary: Build Powerful Web Applications: Simple, Scalable, and Fully Customizable

@@ -232,6 +232,7 @@ Author-email: Simone Giacomelli <simone.giacomelli@gmail.com>

Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: playwright; extra == "test"
Requires-Dist: pytest-playwright; extra == "test"
Requires-Dist: pytest-xvirt; extra == "test"
Requires-Dist: libcst==1.4.0; extra == "test"
Requires-Dist: libcst==1.6.0; extra == "test"
Requires-Dist: rope==1.13.0; extra == "test"

@@ -246,4 +247,6 @@ Provides-Extra: dev

Requires-Dist: build; extra == "pypi"
Provides-Extra: stubs
Requires-Dist: pyodide-stubs; extra == "stubs"
Provides-Extra: all
Requires-Dist: wwwpy[dev,pypi,test]; extra == "all"
Requires-Dist: wwwpy[dev,pypi,stubs,test]; extra == "all"
Dynamic: license-file

@@ -250,0 +253,0 @@

@@ -6,3 +6,3 @@ tornado==6.4.2

[all]
wwwpy[dev,pypi,test]
wwwpy[dev,pypi,stubs,test]

@@ -19,9 +19,13 @@ [dev]

[stubs]
pyodide-stubs
[test]
tox
pytest
pytest-asyncio
playwright
pytest-playwright
pytest-xvirt
libcst==1.4.0
libcst==1.6.0
rope==1.13.0

@@ -33,2 +33,3 @@ LICENSE

src/wwwpy/common/asynclib.py
src/wwwpy/common/attr_lib.py
src/wwwpy/common/collectionlib.py

@@ -90,2 +91,16 @@ src/wwwpy/common/detect.py

src/wwwpy/common/designer/ui/svg.py
src/wwwpy/common/designer/ui/tree_node.py
src/wwwpy/common/designer/ui/icons/__init__.py
src/wwwpy/common/designer/ui/icons/all_icons.py
src/wwwpy/common/designer/ui/icons/events.svg
src/wwwpy/common/designer/ui/icons/jb/console_dark.svg
src/wwwpy/common/designer/ui/icons/jb/project@20x20_dark.svg
src/wwwpy/common/designer/ui/icons/jb/properties_dark.svg
src/wwwpy/common/designer/ui/icons/jb/pythonPackages_dark.svg
src/wwwpy/common/designer/ui/icons/jb/python_stroke.svg
src/wwwpy/common/designer/ui/icons/jb/services_dark.svg
src/wwwpy/common/designer/ui/icons/jb/structure@20x20_dark.svg
src/wwwpy/common/designer/ui/icons/jb/todo@20x20_dark.svg
src/wwwpy/common/designer/ui/icons/jb/toolWindowComponents@20x20_dark.svg
src/wwwpy/common/designer/ui/icons/jb/toolWindowComponents_dark.svg
src/wwwpy/common/filesystem/__init__.py

@@ -203,7 +218,5 @@ src/wwwpy/common/filesystem/sync/__init__.py

src/wwwpy/remote/designer/ui/quickstart_ui.py
src/wwwpy/remote/designer/ui/searchable_combobox.py
src/wwwpy/remote/designer/ui/searchable_combobox2.py
src/wwwpy/remote/designer/ui/searchable_list_1.py
src/wwwpy/remote/designer/ui/svg_icon.py
src/wwwpy/remote/designer/ui/toolbox.py
src/wwwpy/remote/designer/ui/window_component.py

@@ -214,2 +227,5 @@ src/wwwpy/remote/designer/ui/system_tools/__init__.py

src/wwwpy/remote/designer/ui/system_tools/system_versions.py
src/wwwpy/remote/designer/ui/tree/__init__.py
src/wwwpy/remote/designer/ui/tree/custom_tree.py
src/wwwpy/remote/designer/ui/tree/custom_tree_demo.py
src/wwwpy/server/__init__.py

@@ -216,0 +232,0 @@ src/wwwpy/server/__main__.py

@@ -1,3 +0,3 @@

__version__ = "0.1.85"
git_hash_short = "de8c216"
git_hash = "de8c2161a0c53284014bbbc2bd5e92f7087227b7"
__version__ = "0.1.86"
git_hash_short = "811ea0e"
git_hash = "811ea0e91bcb2135ffe67865b690c52145cb824d"

@@ -0,1 +1,2 @@

import json
import textwrap

@@ -14,4 +15,4 @@ from typing import List, Tuple

python: str,
jspi=False,
zip_route_path: str = '/wwwpy/bundle.zip',
packages: list[str] | None = None,
html: str = f'<!DOCTYPE html><h1>Loading...</h1><script>{bootstrap_javascript_placeholder}</script>',

@@ -38,3 +39,3 @@ ) -> Tuple[HttpRoute, HttpRoute]:

javascript = get_javascript_for(bootstrap_python, jspi)
javascript = get_javascript_for(bootstrap_python, packages=packages)
html_replaced = html.replace(bootstrap_javascript_placeholder, javascript)

@@ -45,7 +46,11 @@ bootstrap_route = HttpRoute('/', lambda request, resp: resp(HttpResponse.text_html(html_replaced)))

def get_javascript_for(python_code: str, jspi=False) -> str:
load = '' if not jspi else '{enableRunUntilComplete: true}'
def get_javascript_for(python_code: str, packages: list[str] = None) -> str:
# see https://pyodide.org/en/stable/usage/api/js-api.html#globalThis.loadPyodide
loadPyodide_options = {
'convertNullToNone': True,
'packages': packages or [],
}
return (_js_content
.replace('# python replace marker', python_code)
.replace('`# load option marker`', load))
.replace('`# load option marker`', json.dumps(loadPyodide_options)))

@@ -58,3 +63,3 @@

let script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.6/full/pyodide.js';
script.src = 'https://cdn.jsdelivr.net/pyodide/v0.28.1/full/pyodide.js';
script.onload = async () => {

@@ -61,0 +66,0 @@ let pyodide = await loadPyodide(`# load option marker`);

from __future__ import annotations
from typing import Callable, TypeVar, Any, Generic, Collection
from typing import Callable, TypeVar, Collection, Generic

@@ -38,1 +38,83 @@ T = TypeVar('T')

return self._map.get(key)
from collections import UserList
class ObservableList(UserList[T], Generic[T]):
def _item_added(self, item, index):
pass
def _item_removed(self, item, index):
pass
def append(self, item):
super().append(item)
self._item_added(item, len(self.data) - 1)
def extend(self, items):
start = len(self.data)
super().extend(items)
for i, item in enumerate(items, start):
self._item_added(item, i)
def insert(self, index, item):
super().insert(index, item)
self._item_added(item, index)
def remove(self, item):
idx = self.data.index(item)
super().remove(item)
self._item_removed(item, idx)
def pop(self, index=-1):
idx = index if index >= 0 else len(self.data) + index
item = super().pop(index)
self._item_removed(item, idx)
return item
def clear(self):
for i, item in enumerate(self.data):
self._item_removed(item, i)
super().clear()
def __setitem__(self, index, value):
if isinstance(index, slice):
old = self.data[index]
super().__setitem__(index, value)
for i, item in enumerate(value, index.start or 0):
self._item_added(item, i)
for i, item in enumerate(old, index.start or 0):
self._item_removed(item, i)
else:
idx = index if index >= 0 else len(self.data) + index
old = self.data[index]
super().__setitem__(index, value)
self._item_removed(old, idx)
self._item_added(value, idx)
def __delitem__(self, index):
if isinstance(index, slice):
for i, item in enumerate(self.data[index], index.start or 0):
self._item_removed(item, i)
else:
idx = index if index >= 0 else len(self.data) + index
item = self.data[index]
self._item_removed(item, idx)
super().__delitem__(index)
def __iadd__(self, other):
self.extend(other)
return self
def __imul__(self, n):
original = list(self.data)
for _ in range(n - 1):
self.extend(original)
return self
def __add__(self, other):
new = type(self)(self.data + list(other))
for i, item in enumerate(other, len(self.data)):
new._item_added(item, i)
return new

@@ -1,1 +0,1 @@

pypi_packages = ['libcst==1.4.0', 'rope==1.13.0']
pypi_packages = ['libcst==1.6.0', 'rope==1.13.0']

@@ -15,3 +15,3 @@ from __future__ import annotations

from wwwpy.common.designer.element_library import ElementDefBase
from wwwpy.common.designer.html_edit import Position, html_add_indexed, html_remove_indexed
from wwwpy.common.designer.html_edit import Position, html_add_indexed, html_remove_indexed, HtmlAddResult
from wwwpy.common.designer.html_locator import NodePath, IndexPath, data_name

@@ -107,2 +107,9 @@

position: Position) -> AddResult | AddFailed:
logger.debug(f'Adding element `{edb.tag_name}` of type `{edb.python_type}` to class {class_name}'
f' at index path {index_path} position {position}')
class_info = code_info.class_info(source_code, class_name)
if class_info is None:
print(f'Class {class_name} not found inside source ```{source_code}```')
return AddFailed(Exception(f'Class not found `{class_name}`'), '', '')
source_code_orig = source_code

@@ -117,6 +124,2 @@ try:

source_code = ensure_imports(source_code, imp_tuple)
class_info = code_info.class_info(source_code, class_name)
if class_info is None:
print(f'Class {class_name} not found inside source ```{source_code}```')
return None

@@ -128,20 +131,14 @@ attr_name = class_info.next_attribute_name(edb.tag_name)

Attribute(attr_name, type_name, 'wpc.element()'))
changed_html = []
html_add_res: HtmlAddResult | None = None
def manipulate_html(html):
add = html_add_indexed(html, named_html, index_path, position)
changed_html.append(add)
return add
nonlocal html_add_res
html_add_res = html_add_indexed(html, named_html, index_path, position)
return html_add_res.html
source2 = html_string_edit(source1, class_name, manipulate_html)
new_tree = html_parser.html_to_tree(source2)
if position == Position.afterbegin:
indexes = index_path + [0]
elif position == Position.beforeend:
indexes = index_path + [-1]
if html_add_res is None or not html_add_res.success:
result = AddFailed(Exception('Failed to add element'), '', '')
else:
displacement = 0 if position == Position.beforebegin else 1
indexes = index_path[0:-1] + [index_path[-1] + displacement]
new_node_path = html_locator.tree_to_path(new_tree, indexes)
result = AddResult(source2, new_node_path, changed_html[0])
result = AddResult(source2, html_add_res.new_node_path, html_add_res.html)
except Exception as e:

@@ -148,0 +145,0 @@ import traceback

@@ -20,5 +20,2 @@ from __future__ import annotations

class LocatorNode:
# locator: Locator
# children: list[LocatorNode]
# cst_node: CstNode
_cst_children: list[CstNode]

@@ -44,11 +41,2 @@

# class LocatorTree(UserList[LocatorNode]):
# parent: CompInfo
# cst_tree: CstTree
#
# def __init__(self, parent: CompInfo, cst: CstTree):
# nodes = (LocatorNode())
# super().__init__()
# todo extend CompInfo such as it provides the means for CompStructure component to show

@@ -64,5 +52,24 @@ # the tree structure of the component's HTML.

path: Path
cst_tree: CstTree
source_code: str
_html_found: bool = None
@cached_property
def html(self) -> str:
html = html_from_source(self.source_code, self.class_name)
self._html_found = html is not None
if html is None:
logger.debug(f'Cannot find html for {self.class_name} in {self.path}')
return ''
return html
@property
def html_found(self) -> bool:
x = self.html
return self._html_found
@cached_property
def cst_tree(self) -> CstTree:
return html_to_tree(self.html)
@cached_property
def locator_root(self) -> LocatorNode:

@@ -87,3 +94,6 @@ """The CST tree of the component's HTML.

for path in sorted(folder.glob('*.py'), key=lambda p: p.stem):
yield from iter_comp_info(path, package + '.' + path.stem)
infos = iter_comp_info(path, package + '.' + path.stem)
for info in infos:
if info.html_found:
yield info

@@ -96,14 +106,3 @@

ci = code_info.info(source_code)
return (c for c in (_to_comp_info(source_code, path, cl, package) for cl in ci.classes) if c is not None)
def _to_comp_info(source_code: str, path: Path, cl: code_info.ClassInfo, package: str) -> CompInfo | None:
class_name = cl.name
html = html_from_source(source_code, class_name)
if html is None:
logger.warning(f'Cannot find html for {class_name} in {path}')
return None
cst_tree = html_to_tree(html)
return CompInfo(package, class_name, cl.tag_name, path, cst_tree)
return (c for c in (CompInfo(package, cl.name, cl.tag_name, path, source_code) for cl in ci.classes) if
c is not None)

@@ -328,3 +328,4 @@ from typing import List

'progress': lambda: f'<progress data-name="{name}" value="70" max="100">70%</progress>',
'textarea': _def(placeHolder=True, inner='', add='rows="6" wrap="off" style="width: 100%"'),
'textarea': _def(placeHolder=True, inner='',
add='rows="6" wrap="off" style="width: 100%; box-sizing: border-box"'),
'select': _def(inner='''

@@ -331,0 +332,0 @@ <option value="option1">Option 1</option>

@@ -154,2 +154,4 @@ from __future__ import annotations

value = node.attributes.get(attribute_def.name, None)
if value is not None:
value = escape_string(value)
attribute_editor = AttributeEditor(attribute_def, exists, value,

@@ -190,2 +192,4 @@ self._attribute_set_value, self._attribute_remove)

def _attribute_set_value(self, attribute_editor: AttributeEditor, value: str | None):
if value is not None:
value = unescape_string(value)

@@ -192,0 +196,0 @@ def _html_manipulate(html: str) -> str:

"""This module contains the HTML string manipulator functions."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum

@@ -28,6 +29,19 @@ from html import escape

def html_add_indexed(html: str, add: str, index_path: IndexPath, position: Position) -> str:
@dataclass
class HtmlAddResult:
html: str
new_node_path: NodePath | None
@property
def success(self) -> bool:
return self.new_node_path is not None
def html_add_indexed(html: str, add: str, index_path: IndexPath, position: Position) -> HtmlAddResult:
check_node_path(index_path)
"""This function adds an HTML piece to the specified position in the HTML string."""
def fail() -> HtmlAddResult:
return HtmlAddResult(html, None)
start, end = html_locator.locate_span_indexed(html, index_path)

@@ -43,3 +57,3 @@

if node is None or node.content_span is None:
return html
return fail()
index = node.content_span[0]

@@ -50,3 +64,3 @@ elif position == Position.beforeend:

if node is None or node.content_span is None:
return html
return fail()
index = node.content_span[1]

@@ -56,5 +70,18 @@ else:

return html[:index] + add + html[index:]
new_html = html[:index] + add + html[index:]
import wwwpy.common.designer.html_parser as html_parser
new_tree = html_parser.html_to_tree(new_html)
if position == Position.afterbegin:
indexes = index_path + [0]
elif position == Position.beforeend:
indexes = index_path + [-1]
else:
displacement = 0 if position == Position.beforebegin else 1
indexes = index_path[0:-1] + [index_path[-1] + displacement]
new_node_path = html_locator.tree_to_path(new_tree, indexes)
return HtmlAddResult(new_html, new_node_path)
def html_edit(html: str, edit: str, node_path: NodePath) -> str:

@@ -114,3 +141,2 @@ """This function edits the HTML string at the specified path."""

value_present = cst_attr.value_span is not None

@@ -117,0 +143,0 @@ x = cst_attr.name_span[0]

from __future__ import annotations
import json
import logging

@@ -40,2 +39,3 @@ from dataclasses import dataclass

def path_to_index(path: NodePath) -> IndexPath:

@@ -50,31 +50,7 @@ return [node.child_index for node in path]

def node_path_serialize(path: NodePath) -> str:
return json.dumps([node.__dict__ for node in path])
def node_path_deserialize(serialized: str) -> NodePath:
node_dicts = json.loads(serialized)
return [Node(**node_dict) for node_dict in node_dicts]
def locate_node(html: str, path: NodePath) -> CstNode | None:
cst_tree = html_to_tree(html)
target_node = _locate_node_rec(cst_tree, path)
logger.debug(f'locate_node {path} -> {target_node} for html=```{html}```')
return target_node
index = path_to_index(path)
return locate_node_indexed(html, index)
def _locate_node_rec(nodes: CstTree, path: NodePath, depth: int = 0) -> CstNode | None:
if depth >= len(path):
return None
target_node = path[depth]
if target_node.child_index < 0 or target_node.child_index >= len(nodes):
return None
node = nodes[target_node.child_index]
if depth == len(path) - 1:
return node
return _locate_node_rec(node.children, path, depth + 1)
def locate_node_indexed(html: str, index_path: IndexPath) -> CstNode | None:

@@ -89,3 +65,7 @@ check_node_path(index_path)

child_index = path[depth]
if child_index < 0:
child_index = len(nodes) + child_index
if child_index < 0 or child_index >= len(nodes):
logger.warning(f'Child index {child_index} out of bounds for nodes'
f' {len(nodes)} at depth {depth} in path {path}')
return None

@@ -106,4 +86,4 @@ node = nodes[child_index]

"""
node = locate_node(html, path)
index = path_to_index(path)
node = locate_node_indexed(html, index)
return node.span if node else None

@@ -110,0 +90,0 @@

@@ -408,3 +408,3 @@ """A parser for HTML and XHTML."""

gtpos = rawdata.find('>', namematch.end())
self.handle_endtag_extended(tagname, False)
self.handle_endtag_extended(tagname, False, None)
self.handle_endtag(tagname)

@@ -419,3 +419,3 @@ return gtpos + 1

self.handle_endtag_extended(elem, False)
self.handle_endtag_extended(elem, False, match)
self.handle_endtag(elem)

@@ -429,3 +429,3 @@ self.clear_cdata_mode()

self.handle_starttag_extended(tag, attrs, attrs_extended, True)
self.handle_endtag_extended(tag, True)
self.handle_endtag_extended(tag, True, None)
self.handle_endtag(tag)

@@ -442,3 +442,3 @@

# CUSTOMIZED
def handle_endtag_extended(self, tag, autoclosing):
def handle_endtag_extended(self, tag, autoclosing, match):
pass

@@ -445,0 +445,0 @@

@@ -72,5 +72,9 @@ from __future__ import annotations

def html_to_tree(html: str) -> CstTree:
cache = _html_tree_cache.get(html, None)
if cache is not None:
return cache
parser = _PositionalHTMLParser(html)
parse = parser.parse()
_complete_tree_data(html, parse)
_html_tree_cache[html] = parse
return parse

@@ -122,5 +126,12 @@

def handle_endtag_extended(self, tag, autoclosing):
def handle_endtag_extended(self, tag, autoclosing, match):
if autoclosing:
return
if self._unexpected_endtag(tag):
if match is not None:
group0 = match.group(0)
self.handle_data(group0)
else:
self.handle_data(f'</{tag}>')
return
if tag in self.void_tags: # a void tag with end tag

@@ -168,2 +179,8 @@ return

def _unexpected_endtag(self, tag: str) -> bool:
if not self.stack:
return True
if self.stack[-1].tag_name != tag:
return True
return False

@@ -180,1 +197,27 @@ def _complete_tree_data(html: str, tree: CstTree, parent: CstNode | None = None, level: int = 0):

_complete_tree_data(html, node.children, node, level + 1)
from collections import OrderedDict
class LRUCache(OrderedDict):
def __init__(self, capacity):
super().__init__()
self.capacity = capacity
def __getitem__(self, key):
value = super().__getitem__(key)
self.move_to_end(key)
return value
def __setitem__(self, key, value):
if key in self:
super().__setitem__(key, value)
self.move_to_end(key)
else:
if len(self) >= self.capacity:
self.popitem(last=False)
super().__setitem__(key, value)
_html_tree_cache = LRUCache(200)

@@ -34,8 +34,8 @@ from __future__ import annotations

def get_all_paths_with_hashes(path: str | Path) -> set[tuple[str, str | None]]:
path = Path(path)
base_path = Path(path)
paths = set()
for path in path.rglob('*'):
relative_path = path.relative_to(path)
if path.is_file():
file_hash = get_file_hash(path)
for entry in base_path.rglob('*'):
relative_path = entry.relative_to(base_path)
if entry.is_file():
file_hash = get_file_hash(entry)
paths.add((str(relative_path), file_hash))

@@ -42,0 +42,0 @@ else:

@@ -56,2 +56,6 @@ import dataclasses

continue
if e.event_type == 'created' and not e.is_directory:
# treat created files as modified because sometimes we do not get 'modified' events
e = dataclasses.replace(e, event_type='modified')
rel = e.relative_to(fs)

@@ -58,0 +62,0 @@ if rel.src_path == '' or rel.src_path == '.':

from collections import defaultdict
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Callable, List, Optional
from contextlib import contextmanager

@@ -88,3 +88,9 @@

def is_attr_monitored(name: str) -> bool:
return not name.startswith("_")
def new_setattr(self, name, value):
if not is_attr_monitored(name):
return original_setattr(self, name, value)
old_value = getattr(self, name, None)

@@ -102,3 +108,3 @@ original_setattr(self, name, value)

m = Monitor()
instance.__instance_monitor_attr = m
setattr(instance, __instance_monitor_attr, m)
return m

@@ -122,2 +128,3 @@

# todo rename to group_notifications ?
@contextmanager

@@ -124,0 +131,0 @@ def group_changes(instance):

@@ -22,4 +22,4 @@ from __future__ import annotations

# _delete_empty_project(directory)
logger.info(f'Quickstart applying {quickstart_name} to {directory}')
shutil.copytree(source, directory, dirs_exist_ok=True)
logger.warning(f'Quickstart applied {quickstart_name} to {directory}')
# print_tree(directory, logger.warning)

@@ -26,0 +26,0 @@

@@ -5,6 +5,6 @@ from js import document

async def main():
from wwwpy.remote import shoelace
shoelace.setup_shoelace()
from wwwpy.remote import simple_dark_theme
simple_dark_theme.setup()
from . import component1 # for component registration
document.body.innerHTML = '<component-1></component-1>'

@@ -132,3 +132,3 @@ from __future__ import annotations

_eventbus: EventBus = injector.field()
span1: js.HTMLElement = wpc.element()
_new_component: js.HTMLElement = wpc.element()

@@ -154,3 +154,3 @@ def init_component(self):

<div>
<div data-name="span1">Create new Component</div>
<button data-name="_new_component">Create new Component</button>
<div data-name="_div"></div>

@@ -182,3 +182,3 @@ </div>

async def span1__click(self, event):
async def _new_component__click(self, event):
logger.debug(f'{inspect.currentframe().f_code.co_name} event fired %s', event)

@@ -185,0 +185,0 @@ if js.window.confirm('Add new component file?\nIt will be added to your "remote" folder.'):

@@ -42,19 +42,8 @@ import dataclasses

assert isinstance(locator_event, LocatorEvent), f'Expected LocatorEvent, got {type(locator_event)}'
tool = self._tool
if self._is_recursive(locator_event):
logger.debug(f'Ignoring hover on {locator_event} because it is recursive')
tool.hide()
return
if not tool.element.isConnected:
js.document.body.appendChild(tool.element)
tool.set_reference_geometry2(
locator_event.main_element.getBoundingClientRect(),
locator_event.position()
)
tool.show()
self._setup_tool(locator_event)
def on_submit(self, locator_event: LocatorEvent) -> bool:
if self._is_recursive(locator_event):
logger.debug(f'Ignoring submit on {locator_event} because it is recursive')
self._tool.hide()
return False

@@ -65,6 +54,17 @@ self.add_element(locator_event.locator, locator_event.position(), self.element_def)

def _setup_tool(self, locator_event: LocatorEvent):
tool = self._tool
if not tool.element.isConnected:
js.document.body.appendChild(tool.element)
tool.set_reference_geometry2(locator_event.main_element.getBoundingClientRect(), locator_event.position())
tool.show()
def _is_recursive(self, locator_event: LocatorEvent) -> bool:
rec = locator_event.locator.class_full_name == self.element_def.python_type
logger.debug(
f'Recursive: {rec} {locator_event.locator.class_full_name} is recursive against {self.element_def.python_type}')
if rec:
self._tool.hide()
logger.debug(f'Ignoring hover on {locator_event} because it is recursive')
else:
logger.debug(
f'Recursive: {rec} {locator_event.locator.class_full_name} is recursive against {self.element_def.python_type}')
return rec

@@ -71,0 +71,0 @@

@@ -68,2 +68,3 @@ from __future__ import annotations

if event:
logger.debug(f'Intent.on_hover_js: {event}')
self.on_hover(event)

@@ -75,2 +76,3 @@

if event:
logger.debug(f'Intent.on_submit_js: {event}')
return self.on_submit(event)

@@ -77,0 +79,0 @@ return False

@@ -34,7 +34,7 @@ import logging

<div slot="header">Structure</div>
<wwwpy-comp-structure style="height: 250px; display: flex; overflow: scroll"></wwwpy-comp-structure>
<wwwpy-comp-structure style="height: 150px; display: flex; overflow: auto"></wwwpy-comp-structure>
</wwwpy-accordion-section>
<wwwpy-accordion-section expanded>
<div slot="header">Attributes/Events</div>
<wwwpy-property-editor data-name="_property_editor" style="height: 250px; display: flex; overflow: scroll"></wwwpy-property-editor>
<wwwpy-property-editor data-name="_property_editor" style="height: 250px; display: flex; overflow: auto"></wwwpy-property-editor>
</wwwpy-accordion-section>

@@ -41,0 +41,0 @@ <wwwpy-accordion-section expanded>

@@ -24,3 +24,3 @@ from __future__ import annotations

_active: bool
border: int = 5
host_style: dict[str, str]

@@ -38,10 +38,44 @@ @classmethod

self.element.shadowRoot.innerHTML = """
<style data-name="_style"></style>
<style data-name="_style">
:host { display: flex; border: 0px solid transparent ; --svg-primary-color: #2B2D30; }
:host(:hover) { --svg-primary-color: #3C3E41; }
svg { display: block }
</style>
<div data-name="_div"></div>
"""
self.active = False
self.host_style: dict[str, str] = dict()
self.host_style['border'] = '5px solid transparent'
def _set_style(self, source='na'):
color, hover_color = (_BLUE, '') if self.active else (_BGRD, _GRAY)
logger.warning(f'_set_style: {source}')
sheet = self._style.sheet
js.console.log('=' * 10, self._style, sheet)
if not sheet or not sheet.cssRules:
logger.warning('No CSS rules found in the style element.')
return
cssRules = list(self._style.sheet.cssRules)
host_normal = cssRules[0]
host_normal.style.setProperty('--svg-primary-color', color)
for key, value in self.host_style.items():
host_normal.style.setProperty(key, value)
host_hover = cssRules[1]
host_hover.style.setProperty('--svg-primary-color', hover_color)
return
# language=html
hover_style = ":host(:hover) { --svg-primary-color: %s; }" % (hover_color,) if hover_color else ''
s = ('svg { display: block }\n' +
':host { display: flex; border: %spx solid transparent }\n' % self.border +
':host { --svg-primary-color: %s; }\n' % color + hover_style)
logger.debug(f'set_style: `{s}`')
self._style.innerHTML = s
def load_svg_str(self, svg: str):
self._div.innerHTML = add_rounded_background2(svg, 'var(--svg-primary-color)')
self._set_style()

@@ -57,15 +91,8 @@ @property

self._active = value
root, hover = (_BLUE, '') if value else (_BGRD, _GRAY)
self._set_style(root, hover)
self._set_style('active')
def _set_style(self, color: str, hover_color: str):
# language=html
hover_style = ":host(:hover) { --svg-primary-color: %s; }" % (hover_color,) if hover_color else ''
s = ('svg { display: block }\n' +
':host { border: %spx solid transparent }\n' % self.border +
':host { --svg-primary-color: %s; }\n' % color + hover_style)
logger.debug(f'set_style: `{s}`')
self._style.innerHTML = s
def connectedCallback(self):
self._set_style('connectedCallback')
def _div__click(self, event):
self.active = not self.active

@@ -13,2 +13,5 @@ import js

<style>
html { box-sizing: border-box; }
*, *:before, *:after { box-sizing: inherit; }
:root {

@@ -15,0 +18,0 @@ color-scheme: dark;

@@ -117,2 +117,4 @@ import inspect

from wwwpy.remote.component import Component
class HolderWidget(Widget):

@@ -124,4 +126,4 @@ def __init__(self, html: str = ''):

def show(self, widget: Widget):
widget.holder = self
def show(self, item: Widget | Component):
item.holder = self
c = self._custom_holder()

@@ -131,10 +133,9 @@ while c.hasChildNodes():

self._remove(widget)
self.stack.append(widget)
from wwwpy.remote.component import Component
if isinstance(widget, Component):
c.append(widget.element)
self._remove(item)
self.stack.append(item)
if isinstance(item, Component):
c.append(item.element)
else:
widget.append_to(c)
self.on_show(widget)
item.append_to(c)
self.on_show(item)

@@ -141,0 +142,0 @@ def close(self, widget: Widget):

@@ -54,3 +54,4 @@ from __future__ import annotations

playwright = sync_playwright().start()
launch_args = ['--enable-features=WebAssemblyExperimentalJSPI']
# launch_args = ['--enable-features=WebAssemblyExperimentalJSPI']
launch_args = []
browser = playwright.chromium.launch(headless=args.headless, args=launch_args)

@@ -57,0 +58,0 @@ page = browser.new_page(has_touch=True)

@@ -0,3 +1,3 @@

import logging
import os
import logging
from pathlib import Path

@@ -27,4 +27,5 @@

from wwwpy.remote import micropip_install
await micropip_install('pytest==7.2.2') # didn't work with update to 8.1.1
await micropip_install('pytest-asyncio')
# these two packages are loaded in loadPyodide, to get a performance boost
# await micropip_install('pytest')
# await micropip_install('pytest-asyncio')
await micropip_install('pytest-xvirt')

@@ -31,0 +32,0 @@ import wwwpy.common.designer as des

@@ -93,3 +93,4 @@ from __future__ import annotations

bootstrap_python = f'import remote_test_main; await remote_test_main.main({rootpath},{invocation_dir_json},{args_json})'
webserver.set_routes(*bootstrap_routes(resources, python=bootstrap_python, jspi=True),
packages = ['pytest', 'pytest-asyncio']
webserver.set_routes(*bootstrap_routes(resources, python=bootstrap_python, packages=packages),
xvirt_notify_route, services.route)

@@ -96,0 +97,0 @@ webserver.set_port(find_port()).start_listen()

from __future__ import annotations
from dataclasses import dataclass
from typing import List, Union
import js
from pyodide.ffi import create_proxy
import wwwpy.remote.component as wpc
from wwwpy.remote import dict_to_js
from wwwpy.remote.designer.global_interceptor import InterceptorEvent, GlobalInterceptor
@dataclass
class Actions:
set_input_value = True
hide_dropdown = True
dispatch_change_event = True
class Option:
parent: SearchableComboBox
def __init__(self, text: str = ''):
self.text = text
self.actions = Actions()
self.on_selected = lambda: None
def _on_click(self, event: js.MouseEvent):
js.console.log('_on_click', self.text)
if self.actions.set_input_value:
self.set_input_value()
if self.actions.hide_dropdown:
self.hide_dropdown()
if self.actions.dispatch_change_event:
self.dispatch_change_event()
self.on_selected()
def set_input_value(self):
self.parent.input.value = self.text
def hide_dropdown(self):
self.parent.hide_dropdown()
def dispatch_change_event(self):
self.parent._dispatch_change_event()
def create_element(self) -> js.HTMLElement:
element = self.new_element()
element.addEventListener('click', create_proxy(self._on_click))
return element
def new_element(self) -> js.HTMLElement:
div: js.HTMLDivElement = js.document.createElement('div')
div.textContent = self.text
return div
class SearchableComboBox(wpc.Component, tag_name='wwwpy-searchable-combobox'):
input: js.HTMLInputElement = wpc.element()
search: js.HTMLInputElement = wpc.element()
dropdown: js.HTMLElement = wpc.element()
popup: js.HTMLElement = wpc.element()
def init_component(self):
self.element.attachShadow(dict_to_js({'mode': 'open'}))
# language=html
self.element.shadowRoot.innerHTML = """
<style>
:host {
display: inline-block;
position: relative;
font-family: Arial, sans-serif;
}
input {
width: 90%;
padding: 5px;
background-color: #2a2a2a;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 4px;
font-size: 14px;
}
input::placeholder {
color: #888;
}
.dropdown {
position: absolute;
width: 100%;
max-height: 200px;
resize: both;
overflow-y: auto;
border: 1px solid #444;
background-color: #333;
color: #e0e0e0;
border-radius: 0 0 4px 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.popup {
position: absolute;
z-index: 1000;
display: none;
}
.dropdown * {
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.dropdown div:hover {
background-color: #444;
}
input:focus {
outline: none;
border-color: #666;
box-shadow: 0 0 0 2px rgba(100, 100, 100, 0.3);
}
</style>
<input type="text" placeholder="" data-name="input">
<div data-name="popup" class="popup">
<input type="search" placeholder="Search..." data-name="search">
<div class="dropdown" data-name="dropdown"></div>
</div>
"""
self._interceptor = GlobalInterceptor(self._global_click)
self._options = []
self._dirty = False
self.hide_dropdown()
def _global_click(self, event: InterceptorEvent):
click_inside = self.element.contains(event.target)
js.console.log(f'global click: contains={click_inside}', event.target)
if click_inside:
return
self.hide_dropdown()
def search__input(self, event):
self.filter_options()
def input__change(self, event):
if self._is_free_edit():
self._dispatch_change_event()
def input__mousedown(self, event: js.MouseEvent):
if self._is_free_edit():
return
event.preventDefault()
@property
def _search_visible(self):
return self.popup.style.display != 'none'
def input__click(self, event: js.MouseEvent):
if self._search_visible:
self.hide_dropdown()
else:
self.show_dropdown()
def show_dropdown(self):
if self._is_free_edit():
return
if self._dirty:
self.filter_options()
self.popup.style.display = 'block'
self.search.focus()
self._interceptor.install()
def _is_free_edit(self):
return not self._options
def hide_dropdown(self):
self.popup.style.display = 'none'
self._interceptor.uninstall()
def set_options(self, options: List[Union[str, Option]]):
self._options = options
self._dirty = True
def filter_options(self, event=None):
self._dirty = False
filter_text = self.search.value.lower()
self.dropdown.innerHTML = ''
for option in self._options:
if isinstance(option, str):
option = Option(option)
option.parent = self
option: Option
if filter_text in option.text.lower():
self.dropdown.appendChild(option.create_element())
if not self.dropdown.hasChildNodes():
div: js.HTMLDivElement = js.document.createElement('div')
div.textContent = 'No results'
div.style.textAlign = 'center'
div.style.fontStyle = 'italic'
div.style.fontWeight = 'bold'
div.style.pointerEvents = 'none'
self.dropdown.appendChild(div)
def select_option(self, option):
js.console.log(f'option: {option}')
self.input.value = option
self.hide_dropdown()
self._dispatch_change_event()
def _dispatch_change_event(self):
self.element.dispatchEvent(js.CustomEvent.new('wp-change', {'detail': self.input.value}))
from __future__ import annotations
import asyncio
import logging
from dataclasses import dataclass, field
from functools import partial
from typing import Optional, Tuple, List
import js
from js import document, console, Event, HTMLElement, window
from pyodide.ffi import create_proxy
import wwwpy.remote.component as wpc
from wwwpy.common import state, modlib
from wwwpy.common.designer import element_library
from wwwpy.common.designer.canvas_selection import CanvasSelection, CanvasSelectionChangeEvent
from wwwpy.common.designer.code_edit import add_element, AddResult, AddFailed
from wwwpy.common.designer.element_library import Help, ElementDef
from wwwpy.common.designer.html_edit import Position
from wwwpy.common.designer.html_locator import path_to_index
from wwwpy.common.designer.locator_lib import Locator
from wwwpy.common.injectorlib import inject
from wwwpy.remote import dict_to_js
from wwwpy.remote.designer.drop_zone import DropZone, DropZoneHover
from wwwpy.remote.designer.global_interceptor import GlobalInterceptor, InterceptorEvent
from wwwpy.remote.designer.helpers import _element_lbl, _help_button, info_link, _help_url
from wwwpy.remote.designer.ui.property_editor import PropertyEditor
from wwwpy.remote.designer.ui.window_component import WindowComponent
from wwwpy.remote.designer.ui.window_component import new_window, Geometry
from wwwpy.server.designer import rpc
from . import filesystem_tree
from .help_icon import HelpIcon # noqa
from .mailto_component import MailtoComponent
from .system_tools.logger_levels import LoggerLevelsComponent
from ..locator_js import locator_from
logger = logging.getLogger(__name__)
@dataclass
class ToolboxState:
geometry: Tuple[int, int, int, int] = field(default=(30, 200, 400, 330))
toolbox_search: str = ''
selected_element_path: Optional[Locator] = None
@dataclass
class MenuMeta:
label: str
html: str
always_visible: bool = False
p_element: js.HTMLElement = None
help: Help = None
def menu(label, always_visible=False):
def wrapped(fn, label=label):
help = None
if isinstance(label, Help):
help = label
label = label.description
fn.label = label
fn.meta = MenuMeta(label, label, always_visible, help=help)
return fn
return wrapped
class ToolboxComponent(wpc.Component, tag_name='wwwpy-toolbox'):
_title: js.HTMLDivElement = wpc.element()
body: HTMLElement = wpc.element()
inputSearch: js.HTMLInputElement = wpc.element()
_window: WindowComponent = wpc.element()
property_editor: PropertyEditor = wpc.element()
_select_element_btn: js.HTMLElement = wpc.element()
_select_clear_btn: js.HTMLElement = wpc.element()
components_marker = '-components-'
_toolbox_state: ToolboxState = None
_canvas_selection: CanvasSelection = inject()
@property
def visible(self) -> bool:
return self._window.element.style.display != 'none'
@visible.setter
def visible(self, value: bool):
self._window.element.style.display = 'block' if value else 'none'
def init_component(self):
self.element.attachShadow(dict_to_js({'mode': 'open'}))
# language=html
self.element.shadowRoot.innerHTML = """
<style>
.two-column-layout {
display: flex;
flex-wrap: wrap;
}
.two-column-layout p {
width: calc(50% - 10px); /* 50% width minus half of the gap */
margin: 0 20px 10px 0; /* Right margin creates gap between columns */
}
.two-column-layout p:nth-child(even) {
margin-right: 0; /* Removes right margin for every even child */
}
</style>
<wwwpy-window data-name='_window'>
<div slot='title' data-name="_title" style='text-align: center'></div>
<div style="text-align: center; padding: 8px">
<button data-name="_select_element_btn">Select element...</button>
<button data-name="_select_clear_btn">Clear selection</button>
<wwwpy-property-editor data-name="property_editor"></wwwpy-property-editor>
<p><input data-name='inputSearch' type='search' placeholder='type to filter...'></p>
<div data-name='body' class='two-column-layout'></div>
</div>
</wwwpy-window>
"""
import wwwpy
self._title.innerText = f'toolbox - {wwwpy.__banner__}'
def _csce(event: CanvasSelectionChangeEvent):
logger.debug(f'canvas selection changed: {event}')
self._toolbox_state.selected_element_path = event.new
self._restore_selected_element_path()
self._canvas_selection.on_change.add(_csce)
def connectedCallback(self):
self._manage_toolbox_state()
self._window.closable = False
attrs = [v for k, v in vars(self.__class__).items() if hasattr(v, 'label')]
self._all_items: List[MenuMeta] = []
def add_p(menu_meta: MenuMeta, callback): # callback can be async
self._all_items.append(menu_meta)
p: js.HTMLElement = document.createElement('p')
p.style.color = 'white'
menu_meta.p_element = p
p.innerHTML = menu_meta.html
p.addEventListener('click', create_proxy(callback))
def add_comp(element_def: ElementDef):
def _on_hover(drop_zone: DropZone | None):
console.log(f'pointed dropzone: {drop_zone}')
msg = (f'Insert new {element_def.tag_name}')
if drop_zone:
pos = 'before' if drop_zone.position == Position.beforebegin else 'after'
# msg += f' at {drop_zone.position.name} of {drop_zone.element.tagName}'
msg += f' {pos} {drop_zone.element.tagName}'
else:
msg += ' ... select a dropzone on the page.'
self.property_editor.message1div.innerHTML = msg
async def _start_drop_for_comp(event):
_on_hover(None)
res = await _drop_zone_start_selection_async(_on_hover, whole=False)
logger.debug(f'_start_drop_for_comp res={res}')
if res:
self.property_editor.set_state_selection_active()
await self._process_dropzone(res, element_def)
else:
await self._canceled()
element_html = f'<span style="cursor: pointer">{element_def.tag_name}</span> {_help_button(element_def)}'
add_p(MenuMeta(element_def.tag_name, element_html), _start_drop_for_comp)
for member in attrs:
if member.meta.label == self.components_marker:
[add_comp(ele_def) for ele_def in element_library.element_library().elements]
else:
help = member.meta.help
if help:
help_html = '' if not help.url else info_link(help.url)
element_html = f'{help.description} {help_html}'
add_p(MenuMeta(help.description, element_html), partial(member, self))
else:
add_p(member.meta, partial(member, self))
self._update_toolbox_elements()
async def _process_dropzone(self, drop_zone: DropZone, element_def: ElementDef):
el_path = locator_from(drop_zone.element)
if not el_path:
window.alert(f'No component found for dropzone!')
return
logger.debug(f'element_path={el_path}')
file = modlib._find_module_path(el_path.class_module)
old_source = file.read_text()
path_index = path_to_index(el_path.path)
add_result = add_element(old_source, el_path.class_name, element_def, path_index, drop_zone.position)
if isinstance(add_result, AddResult):
logger.debug(f'write_module_file len={len(add_result.source_code)} el_path={el_path}')
new_element_path = Locator(el_path.class_module, el_path.class_name, add_result.node_path,
el_path.origin)
self._toolbox_state.selected_element_path = new_element_path
write_res = await rpc.write_module_file(el_path.class_module, add_result.source_code)
logger.debug(f'write_module_file res={write_res}')
elif isinstance(add_result, AddFailed):
js.alert('Sorry, an error occurred while adding the component.')
_open_error_reporter_window(
'Error report data:\n\n' + add_result.exception_report_b64,
title='Error report add_component - wwwpy'
)
# pre1: js.HTMLElement = js.document.createElement('pre')
# pre1.innerText = self.path.read_text()
# win.element.append(pre1)
def _manage_toolbox_state(self):
self._toolbox_state = state._restore(ToolboxState)
self.inputSearch.value = self._toolbox_state.toolbox_search
self._window.set_geometry(Geometry(*self._toolbox_state.geometry))
def on_toolbar_geometry_change():
g = self._window.geometry()
acceptable_geometry = g.width > 100 and g.height > 100 and g.top > 0 and g.left > 0
if acceptable_geometry:
self._toolbox_state.geometry = self._window.geometry()
self._window.geometry_change_listeners.append(on_toolbar_geometry_change)
self._restore_selected_element_path()
def inputSearch__input(self, e: Event):
self._toolbox_state.toolbox_search = self.inputSearch.value
self._update_toolbox_elements()
def _update_toolbox_elements(self):
def search_match(meta: MenuMeta):
search = self.inputSearch.value.lower()
lbl = meta.label.lower()
starts_with = search.startswith(' ') and lbl.startswith(search.lstrip())
ends_with = search.endswith(' ') and lbl.endswith(search.rstrip())
if starts_with or ends_with:
return True
return search in lbl
self.body.innerHTML = ''
for meta in self._all_items:
p = meta.p_element
if meta.always_visible or search_match(meta):
self.body.appendChild(p)
async def _select_clear_btn__click(self, e: Event):
self._toolbox_state.selected_element_path = None
self._restore_selected_element_path()
async def _select_element_btn__click(self, e: Event):
no_comp = 'Select an element on the page to be inspected and edited...'
self.property_editor.message1div.innerHTML = no_comp
def _on_hover(drop_zone: DropZone | None):
msg = no_comp
if drop_zone:
msg = 'Click to select ' + _element_lbl(drop_zone.element)
self.property_editor.message1div.innerHTML = msg
console.log(f'pointed dropzone: {drop_zone}')
res = await _drop_zone_start_selection_async(_on_hover, whole=True)
if res:
self.property_editor.set_state_selection_active()
self._toolbox_state.selected_element_path = locator_from(res.element)
else:
await self._canceled()
self._restore_selected_element_path()
# @menu(Help("Open errore reporter", ''))
# async def _open_error_reporter(self, e: Event):
# _open_error_reporter_window('test error report body')
@menu(Help('Create new component', _help_url('add_component')))
async def _add_new_component(self, e: Event):
if js.window.confirm('Add new component file?\nIt will be added to your "remote" folder.'):
res = await rpc.add_new_component()
js.window.alert(res)
@menu(Help("Explore local filesystem", _help_url('remote_filesystem')))
async def _browse_local_filesystem(self, e: Event):
from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent
filesystem_tree.show_explorer(DevModeComponent.instance.root_element())
@menu(Help("Python console", ''))
async def _python_console(self, e: Event):
from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent
w1 = new_window('Python console')
from wwwpy.remote.designer.ui.python_console import PythonConsoleComponent
w1.element.append(PythonConsoleComponent().element)
DevModeComponent.instance.root_element().append(w1.element)
@menu(Help("Logger levels", ''))
async def _python_console(self, e: Event):
from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent
w1 = new_window('Logger levels')
w1.element.append(LoggerLevelsComponent().element)
DevModeComponent.instance.root_element().append(w1.element)
@menu(components_marker)
def _drop_zone_start(self, e: Event):
assert False, 'Just a placeholder'
# @menu('handle global click')
def _handle_global_click(self, e: Event):
# add input
def global_click(event: Event):
event.preventDefault()
event.stopImmediatePropagation()
event.stopPropagation()
console.log('global click', event.element)
if self._global_click:
document.removeEventListener('click', self._global_click, True)
self._global_click = None
self._global_click = create_proxy(global_click)
document.addEventListener('click', self._global_click, True)
def _restore_selected_element_path(self):
element_path = self._toolbox_state.selected_element_path
if element_path:
logger.debug(f'restoring selected element path: {element_path}')
if not element_path.valid():
element_path = None
logger.warning(f'invalid element path, setting to None')
if element_path:
self._select_clear_btn.style.visibility = 'visible'
else:
self._select_clear_btn.style.visibility = 'hidden'
self.property_editor.selected_element_path = element_path
async def _canceled(self):
self.property_editor.message1div.innerHTML = 'Operation canceled'
await asyncio.sleep(2)
self.property_editor.message1div.innerHTML = ''
def is_inside_toolbar(element: HTMLElement | None):
if not element:
return False
orig = element
# loop until the root element and see if it is the toolbar
res = False
while element:
if element.tagName.lower() == ToolboxComponent.component_metadata.tag_name:
res = True
break
element = element.parentElement
return res
def _default_drop_zone_accept(drop_zone: DropZone):
element = drop_zone.element
name = element.tagName.lower()
from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent
root_elements = name == 'body' or name == 'html'
wwwpy_elements = name == DevModeComponent.component_metadata.tag_name or is_inside_toolbar(element)
accept = not (root_elements or wwwpy_elements)
return accept
async def _drop_zone_start_selection_async(on_hover: DropZoneHover, whole: bool) -> Optional[DropZone]:
from wwwpy.remote.designer.drop_zone import drop_zone_selector
event = asyncio.Event()
result = []
def intercept_ended(ev: InterceptorEvent):
console.log('intercept_ended', ev.target)
ev.preventAndStop()
selected = drop_zone_selector.stop()
ev.uninstall()
if selected:
selected: DropZone
console.log(
f'\nselection accepted position {selected.position.name}'
f'\ntarget: ', selected.element,
'\nparent: ', selected.element.parentElement,
'\nevent: ', ev.event,
'\ncomposedPath: ', ev.event.composedPath(),
)
result.append(selected)
else:
console.log('intercept_ended - canceled')
event.set()
GlobalInterceptor(intercept_ended, 'pointerdown').install()
click_inter = GlobalInterceptor(lambda ev: ev.preventAndStop(), 'click')
click_inter.install()
drop_zone_selector.start_selector(on_hover, _default_drop_zone_accept, whole=whole)
await event.wait()
console.log('drop_zone_selector event ended')
async def _click_inter_uninstall():
await asyncio.sleep(0.5)
click_inter.uninstall()
asyncio.ensure_future(_click_inter_uninstall())
if len(result) == 0:
return None
return result[0]
def _open_error_reporter_window(body: str, title='Error reporter - wwwpy'):
win = new_window(title, closable=True)
mailto_component = MailtoComponent()
mailto_component.subject = title
mailto_component.recipient = 'simone.giacomelli@gmail.com'
mailto_component.body = body
mailto_component.text_content = 'Click here to send an email with the error report'
mailto_component.element.style.display = 'block'
mailto_component.element.style.textAlign = 'center'
div: js.HTMLElement = js.document.createElement('div')
action = '<h3>Please, help us improve wwwpy by sending this error report</h3>'
div.insertAdjacentHTML('beforeend', action)
div.append(mailto_component.element)
div.style.margin = '15px'
win.element.append(div)
div.insertAdjacentHTML('beforeend', '<br>')
js.document.body.append(win.element)
from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent
DevModeComponent.instance.root_element().append(win.element)