├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .run
├── Python tests (headed) .run.xml
└── Python tests (headless).run.xml
├── LICENSE
├── README.md
├── dev-notes
└── rename-rpc.patch
├── docs
├── cli_usage.md
├── component.md
├── introduction.md
└── rpc.md
├── incubator
├── accordion-custom-elements
│ └── accordion-custom-elements.html
├── draggable-window-system
│ ├── draggable-window.js
│ └── index.html
├── drop-indicator
│ └── index.html
├── getClientRects-vs
│ └── getClientRects-vs.html
├── html-editor-vanilla-js
│ ├── html-editor-claude.html
│ └── html-editor-vanilla-js.html
├── html-tree
│ ├── html-tree-marker-summary-hotzone-claude.html
│ ├── html-tree-marker-summary-hotzone.html
│ └── html-tree.html
├── names-slots
│ └── named-slots-index.html
├── popup-notifications
│ ├── popup-notification-custom-element.html
│ └── popup-notification-library.html
├── pycharm-plugin
│ └── plugin.groovy
└── pytest-xvirt-fix-stacktrace
│ ├── failed-remote-test-pytest-output.txt
│ └── pytest-xvirt-EvtRuntestLogreport.json
├── licenses
├── README.md
└── platformdirs-LICENSE
├── mypy.ini
├── pip-install-all.py
├── preprocessing
├── __init__.py
└── shoelace
│ ├── __init__.py
│ ├── convert.py
│ ├── vscode.html-custom-data.json
│ └── web-types.json
├── pypi_helper.py
├── pypi_helper_test.py
├── pypi_inc_minor.py
├── pypi_upload.py
├── pyproject.toml
├── pytest.ini
├── src
├── wwwpy
│ ├── __init__.py
│ ├── _build_meta.py
│ ├── asgi
│ │ ├── __init__.py
│ │ ├── asgi-compliant.md
│ │ ├── echo_handler.py
│ │ ├── favicon.png
│ │ ├── favicon.txt
│ │ ├── index.html
│ │ ├── main_daphne.py
│ │ ├── main_granian.py
│ │ ├── main_hypercorn.py
│ │ ├── main_tornado.py
│ │ ├── main_uvicorn.py
│ │ └── tornado_asgi_handler.py
│ ├── base_conf.py
│ ├── bootstrap.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── _raise_on_any.py
│ │ ├── asynclib.py
│ │ ├── collectionlib.py
│ │ ├── databind
│ │ │ ├── __init__.py
│ │ │ └── databind.py
│ │ ├── designer
│ │ │ ├── __init__.py
│ │ │ ├── canvas_selection.py
│ │ │ ├── class_path.py
│ │ │ ├── code_edit.py
│ │ │ ├── code_info.py
│ │ │ ├── code_strings.py
│ │ │ ├── comp_info.py
│ │ │ ├── el_common.py
│ │ │ ├── el_shoelace.json
│ │ │ ├── el_shoelace.py
│ │ │ ├── el_standard.py
│ │ │ ├── element_editor.py
│ │ │ ├── element_library.py
│ │ │ ├── html_edit.py
│ │ │ ├── html_locator.py
│ │ │ ├── html_parser.py
│ │ │ ├── html_parser_mod.py
│ │ │ ├── locator_lib.py
│ │ │ ├── log_emit.py
│ │ │ ├── new_component.py
│ │ │ ├── packaging
│ │ │ │ ├── __init__.py
│ │ │ │ ├── package_manager.py
│ │ │ │ └── packages.py
│ │ │ ├── sl_icons.txt
│ │ │ └── ui
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _drop_indicator_svg.py
│ │ │ │ ├── rect_readonly.py
│ │ │ │ ├── rect_readonly_py.py
│ │ │ │ └── svg.py
│ │ ├── detect.py
│ │ ├── escapelib.py
│ │ ├── event_observer.py
│ │ ├── eventbus.py
│ │ ├── exitlib.py
│ │ ├── extension_point.py
│ │ ├── fetch.py
│ │ ├── fetch_debug.py
│ │ ├── files.py
│ │ ├── filesystem
│ │ │ ├── __init__.py
│ │ │ └── sync
│ │ │ │ ├── __init__.py
│ │ │ │ ├── event.py
│ │ │ │ ├── event_invert_apply.py
│ │ │ │ ├── event_rebase.py
│ │ │ │ └── sync_delta2.py
│ │ ├── http_transport.py
│ │ ├── indent.py
│ │ ├── injectorlib.py
│ │ ├── iterlib.py
│ │ ├── loglib.py
│ │ ├── modlib.py
│ │ ├── property_monitor.py
│ │ ├── quickstart
│ │ │ ├── .gitignore
│ │ │ ├── __init__.py
│ │ │ ├── basic
│ │ │ │ ├── readme.txt
│ │ │ │ └── remote
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── component1.py
│ │ │ ├── chat
│ │ │ │ ├── common
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── name.py
│ │ │ │ ├── readme.txt
│ │ │ │ ├── remote
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── component1.py
│ │ │ │ │ └── rpc.py
│ │ │ │ └── server
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── rpc.py
│ │ │ ├── markdown
│ │ │ │ ├── readme.txt
│ │ │ │ └── remote
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── component1.py
│ │ │ └── upload
│ │ │ │ ├── .gitignore
│ │ │ │ ├── readme.txt
│ │ │ │ ├── remote
│ │ │ │ ├── __init__.py
│ │ │ │ ├── component1.py
│ │ │ │ └── upload_component.py
│ │ │ │ └── server
│ │ │ │ ├── __init__.py
│ │ │ │ └── rpc.py
│ │ ├── reloader.py
│ │ ├── result.py
│ │ ├── rpc
│ │ │ ├── __init__.py
│ │ │ ├── custom_loader.py
│ │ │ ├── func_registry.py
│ │ │ ├── hibrid_dispatcher.py
│ │ │ ├── invoker.py
│ │ │ ├── serialization.py
│ │ │ ├── serializer.py
│ │ │ └── v2
│ │ │ │ ├── __init__.py
│ │ │ │ ├── caller_proxy.py
│ │ │ │ └── dispatcher.py
│ │ ├── rpc2
│ │ │ ├── __init__.py
│ │ │ ├── default_skeleton.py
│ │ │ ├── default_stub.py
│ │ │ ├── encoder_decoder.py
│ │ │ ├── skeleton.py
│ │ │ ├── stub.py
│ │ │ ├── transport.py
│ │ │ └── typed_function.py
│ │ ├── settingslib.py
│ │ ├── state.py
│ │ ├── strings.py
│ │ ├── time_logger.py
│ │ ├── tree.py
│ │ └── type_listener.py
│ ├── exceptions.py
│ ├── http.py
│ ├── platformdirs
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── android.py
│ │ ├── api.py
│ │ ├── macos.py
│ │ ├── readme.txt
│ │ ├── unix.py
│ │ ├── version.py
│ │ └── windows.py
│ ├── remote
│ │ ├── __init__.py
│ │ ├── _elementlib.py
│ │ ├── browser_main.py
│ │ ├── component.py
│ │ ├── databind
│ │ │ ├── __init__.py
│ │ │ └── bind_wrapper.py
│ │ ├── designer
│ │ │ ├── __init__.py
│ │ │ ├── dev_mode.py
│ │ │ ├── dev_mode_events.py
│ │ │ ├── di_remote.py
│ │ │ ├── drop_zone.py
│ │ │ ├── global_interceptor.py
│ │ │ ├── helpers.py
│ │ │ ├── locator_js.py
│ │ │ ├── log_redirect.py
│ │ │ ├── rpc.py
│ │ │ └── ui
│ │ │ │ ├── __init__.py
│ │ │ │ ├── accordion_components.py
│ │ │ │ ├── button_tab.py
│ │ │ │ ├── comp_structure.py
│ │ │ │ ├── design_aware.py
│ │ │ │ ├── dev_mode_component.py
│ │ │ │ ├── drag_manager.py
│ │ │ │ ├── element_selector.py
│ │ │ │ ├── filesystem_tree.py
│ │ │ │ ├── floater.py
│ │ │ │ ├── floater_action_band.py
│ │ │ │ ├── floater_drop_indicator.py
│ │ │ │ ├── floater_selection_indicator.py
│ │ │ │ ├── floater_selection_indicator_weird.py
│ │ │ │ ├── help_icon.py
│ │ │ │ ├── intent.py
│ │ │ │ ├── intent_add_element.py
│ │ │ │ ├── intent_manager.py
│ │ │ │ ├── intent_select_element.py
│ │ │ │ ├── locator_event.py
│ │ │ │ ├── mailto_component.py
│ │ │ │ ├── mailto_edit_component.py
│ │ │ │ ├── new_toolbox.py
│ │ │ │ ├── palette.py
│ │ │ │ ├── pointer_api.py
│ │ │ │ ├── property_editor.py
│ │ │ │ ├── pushable_sidebar.py
│ │ │ │ ├── python_console.py
│ │ │ │ ├── quickstart_ui.py
│ │ │ │ ├── searchable_combobox.py
│ │ │ │ ├── searchable_combobox2.py
│ │ │ │ ├── searchable_list_1.py
│ │ │ │ ├── svg_icon.py
│ │ │ │ ├── system_tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── logger_levels.py
│ │ │ │ ├── system_tools_component.py
│ │ │ │ └── system_versions.py
│ │ │ │ ├── toolbox.py
│ │ │ │ └── window_component.py
│ │ ├── eventlib.py
│ │ ├── fetch.py
│ │ ├── files.py
│ │ ├── hotkey.py
│ │ ├── hotkeylib.py
│ │ ├── idbfs.py
│ │ ├── jslib.py
│ │ ├── remote_fixtures.py
│ │ ├── root_path.py
│ │ ├── shoelace.py
│ │ ├── simple_dark_theme.py
│ │ ├── websocket.py
│ │ └── widget.py
│ ├── resources.py
│ ├── rpc.py
│ ├── server
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── asgi.py
│ │ ├── configure.py
│ │ ├── convention.py
│ │ ├── custom_str.py
│ │ ├── designer
│ │ │ ├── __init__.py
│ │ │ ├── dev_mode.py
│ │ │ └── rpc.py
│ │ ├── fetch.py
│ │ ├── filesystem_sync
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── any_observer.py
│ │ │ ├── debouncer.py
│ │ │ ├── debouncer_thread.py
│ │ │ ├── sync_delta.py
│ │ │ ├── sync_zip.py
│ │ │ └── watchdog_debouncer.py
│ │ ├── proxy.py
│ │ ├── pytestlib
│ │ │ ├── __init__.py
│ │ │ ├── playwrightlib.py
│ │ │ ├── pytest_plugin.py
│ │ │ ├── remote_conftest.py
│ │ │ ├── remote_test_main.py
│ │ │ └── xvirt_impl.py
│ │ ├── rpc4tests.py
│ │ ├── settingslib.py
│ │ ├── tcp_port.py
│ │ └── wait_url.py
│ ├── unasync.py
│ ├── webserver.py
│ ├── webservers
│ │ ├── __init__.py
│ │ ├── asgi_webserver.py
│ │ ├── available_webservers.py
│ │ └── tornado.py
│ └── websocket.py
└── wwwpy_plugins
│ ├── README.md
│ └── wwwpy
│ ├── README.md
│ └── __init__.py
├── tests
├── __init__.py
├── available_webservers_test.py
├── common
│ ├── __init__.py
│ ├── _raise_on_any_test.py
│ ├── collectionlib_test.py
│ ├── databind
│ │ ├── __init__.py
│ │ └── test_databind.py
│ ├── designer
│ │ ├── __init__.py
│ │ ├── canvas_selection_test.py
│ │ ├── code_edit_test.py
│ │ ├── code_info_test.py
│ │ ├── code_strings_test.py
│ │ ├── comp_info_test.py
│ │ ├── component_fixture.py
│ │ ├── element_editor_test.py
│ │ ├── element_library_test.py
│ │ ├── html_edit_test.py
│ │ ├── html_locator_test.py
│ │ ├── html_parser_test.py
│ │ ├── locator_lib_test.py
│ │ └── ui
│ │ │ ├── __init__.py
│ │ │ ├── _drop_indicator_svg_test.py
│ │ │ ├── rect_readonly_py_test.py
│ │ │ └── svg_test.py
│ ├── event_observer_test.py
│ ├── eventbus_test.py
│ ├── exitlib_test.py
│ ├── extension_point_test.py
│ ├── files_test.py
│ ├── injectorlib_test.py
│ ├── iterlib_test.py
│ ├── loglib_test.py
│ ├── modlib_test.py
│ ├── package_manager_test.py
│ ├── property_monitor_test.py
│ ├── quickstart_test.py
│ ├── reload
│ │ ├── __init__.py
│ │ ├── reload_1
│ │ │ ├── component.py
│ │ │ └── reload_1_test_disable.py
│ │ ├── reload_2
│ │ │ ├── package2
│ │ │ │ ├── __init__.py
│ │ │ │ ├── class_a.py
│ │ │ │ └── class_b.py
│ │ │ └── reload_2_test_disable.py
│ │ └── reload_test.py
│ ├── reloader_test.py
│ ├── rpc
│ │ ├── __init__.py
│ │ ├── custom_loader_test.py
│ │ ├── support1.py
│ │ ├── support2.py
│ │ ├── support3.py
│ │ ├── test_ast_parser.py
│ │ ├── test_invoker.py
│ │ ├── test_rpc.py
│ │ ├── test_serialization.py
│ │ ├── test_serializer.py
│ │ └── v2
│ │ │ └── caller_proxy_test.py
│ ├── rpc2
│ │ ├── __init__.py
│ │ ├── encoder_decoder_test.py
│ │ ├── rpc_integration_test.py
│ │ ├── stub_generator_test.py
│ │ ├── transport_fake.py
│ │ └── typed_function_test.py
│ ├── settingslib_test.py
│ ├── state_test.py
│ └── version_test.py
├── conftest.py
├── layer
│ ├── LAYERS.md
│ ├── __init__.py
│ ├── layer_2_support
│ │ ├── build_archive
│ │ │ └── simple
│ │ │ │ ├── dir1
│ │ │ │ └── bar.txt
│ │ │ │ └── foo.txt
│ │ └── from_filesystem
│ │ │ ├── one_file
│ │ │ └── foo.py
│ │ │ ├── relative_to
│ │ │ └── yes
│ │ │ │ └── yes.txt
│ │ │ └── resource_filter
│ │ │ └── yes
│ │ │ ├── reject
│ │ │ └── foo.txt
│ │ │ └── yes.txt
│ ├── layer_4_support
│ │ ├── convention_b
│ │ │ └── remote
│ │ │ │ └── __init__.py
│ │ ├── convention_c_async
│ │ │ └── remote
│ │ │ │ └── __init__.py
│ │ └── convention_c_sync
│ │ │ └── remote
│ │ │ └── __init__.py
│ ├── layer_5_support
│ │ ├── rpc_remote
│ │ │ └── remote
│ │ │ │ ├── __init__.py
│ │ │ │ └── rpc.py
│ │ └── rpc_server
│ │ │ ├── remote
│ │ │ └── __init__.py
│ │ │ └── server
│ │ │ ├── __init__.py
│ │ │ └── rpc.py
│ ├── test_dev_mode.py
│ ├── test_hot_reload.py
│ ├── test_layer5_test_pool_change_failure.txt
│ ├── test_layer_1.py
│ ├── test_layer_2.py
│ ├── test_layer_3.py
│ ├── test_layer_4.py
│ └── test_layer_5.py
├── remote
│ ├── README.md
│ ├── __init__.py
│ ├── component_future_test.py
│ ├── component_test.py
│ ├── databind
│ │ ├── __init__.py
│ │ └── test_databind.py
│ ├── designer
│ │ ├── __init__.py
│ │ ├── drop_zone_test.py
│ │ ├── locator_js_test.py
│ │ └── ui
│ │ │ ├── __init__.py
│ │ │ ├── accordion_test.py
│ │ │ ├── button_tab_test.py
│ │ │ ├── drag_manager_test.py
│ │ │ ├── element_selector_test.py
│ │ │ ├── intent_add_element_test.py
│ │ │ ├── intent_manager_test.py
│ │ │ ├── intent_select_element_test.py
│ │ │ ├── locator_event_test.py
│ │ │ ├── palette_test.py
│ │ │ ├── pushable_sidebar_test.py
│ │ │ ├── searchable_combobox_test.py
│ │ │ ├── svg_test.py
│ │ │ └── tool_selection_indicator_test.py
│ ├── element_lib_test.py
│ ├── eventlib_test.py
│ ├── jslib_test.py
│ ├── rpc4tests_helper.py
│ ├── rpc4tests_test.py
│ ├── test_in_pyodide.py
│ ├── test_rpc.py
│ └── test_widget.py
├── server
│ ├── __init__.py
│ ├── cmd_line_args_test.py
│ ├── convention_fixture.py
│ ├── dev_mode_test.py
│ ├── filesystem_sync
│ │ ├── __init__.py
│ │ ├── activity_monitor.py
│ │ ├── activity_monitor_test.py
│ │ ├── any_observer_test.py
│ │ ├── debouncer_test.py
│ │ ├── event_invert_apply_test.py
│ │ ├── event_logger.py
│ │ ├── event_rebase_test.py
│ │ ├── event_test.py
│ │ ├── filesystem_fixture.py
│ │ ├── fs_compare.py
│ │ ├── mutator.py
│ │ ├── sync_fixture.py
│ │ ├── sync_test.py
│ │ ├── time_mock.py
│ │ ├── wakeup_recorder.py
│ │ ├── watchdog-events.txt
│ │ └── watchdog_debouncer_test.py
│ ├── page_fixture.py
│ ├── pytestlib
│ │ ├── __init__.py
│ │ └── xvirt_impl_test.py
│ ├── remote_ui
│ │ ├── __init__.py
│ │ ├── drop_zone_test.py
│ │ └── searchable_combobox_test.py
│ ├── rpc
│ │ ├── __init__.py
│ │ ├── test_invoker.py
│ │ ├── test_rpc.py
│ │ └── test_rpc_sync.py
│ ├── startup
│ │ ├── __init__.py
│ │ └── test_dev_mode.py
│ └── tcp_port_test.py
├── test_unsync.py
└── timeouts.py
└── tox.ini
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 |
4 | on:
5 | push:
6 | branches: [ '*' ]
7 | pull_request:
8 | branches: [ '*' ]
9 | workflow_dispatch:
10 |
11 | env:
12 | FORCE_COLOR: "1" # Make tools pretty.
13 | PLAYWRIGHT_PATCH_TIMEOUT_MILLIS: "45000"
14 |
15 | jobs:
16 | test:
17 | runs-on: ${{ matrix.runs-on }}
18 | strategy:
19 | matrix:
20 | python-version: [ "3.10", "3.11", "3.12", "3.13" ]
21 | runs-on: [ ubuntu-latest, macos-13, windows-latest ]
22 | fail-fast: false
23 | env:
24 | TOX_TESTENV_PASSENV: "XAUTHORITY DISPLAY"
25 |
26 | steps:
27 | - uses: actions/checkout@v4
28 |
29 | - name: Install uv
30 | uses: astral-sh/setup-uv@v5
31 | with:
32 | enable-cache: true
33 | version: "0.6.1"
34 |
35 | - name: Set up Python ${{ matrix.python-version }}
36 | run: uv python install ${{ matrix.python-version }}
37 |
38 | - name: Install tox with tox-uv
39 | run: uv tool install tox --with tox-uv
40 |
41 | - name: Run tests
42 | run: tox -e py
43 |
--------------------------------------------------------------------------------
/.run/Python tests (headed) .run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.run/Python tests (headless).run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/incubator/html-tree/html-tree-marker-summary-hotzone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Minimal Tree Example
7 |
17 |
18 |
19 | Tree Widget via details/summary
20 |
21 |
22 | Fruits
23 |
24 | Citrus
25 |
26 | Orange
27 |
28 |
29 | Lemon
30 |
31 |
32 |
33 | Berries
34 |
35 | Strawberry
36 |
37 |
38 | Blueberry
39 |
40 |
41 |
42 |
43 |
44 | Vegetables
45 |
46 | Root
47 |
48 | Carrot
49 |
50 |
51 | Beetroot
52 |
53 |
54 |
55 | Leafy Greens
56 |
57 | Spinach
58 |
59 |
60 | Lettuce
61 |
62 |
63 |
64 |
65 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/incubator/html-tree/html-tree.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Minimal Tree Example
6 |
16 |
17 |
18 | Tree Widget via details/summary
19 |
20 |
21 | Fruits
22 |
23 | Citrus
24 |
25 | Orange
26 |
27 |
28 | Lemon
29 |
30 |
31 |
32 | Berries
33 |
34 | Strawberry
35 |
36 |
37 | Blueberry
38 |
39 |
40 |
41 |
42 |
43 | Vegetables
44 |
45 | Root
46 |
47 | Carrot
48 |
49 |
50 | Beetroot
51 |
52 |
53 |
54 | Leafy Greens
55 |
56 | Spinach
57 |
58 |
59 | Lettuce
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/incubator/pycharm-plugin/plugin.groovy:
--------------------------------------------------------------------------------
1 | import com.intellij.openapi.actionSystem.AnAction
2 | import com.intellij.openapi.actionSystem.AnActionEvent
3 | import com.intellij.openapi.project.DumbAware
4 |
5 | import static liveplugin.PluginUtil.registerAction
6 | import static liveplugin.PluginUtil.show
7 |
8 | import com.intellij.openapi.actionSystem.CommonDataKeys
9 | import static liveplugin.PluginUtil.*
10 | import com.intellij.openapi.wm.IdeFocusManager
11 |
12 | registerAction("Hello World", "alt shift H") { AnActionEvent event ->
13 | def project = event?.project
14 | def editor = CommonDataKeys.EDITOR.getData(event.dataContext)
15 | if (project == null || editor == null) {
16 | show("No project or editor")
17 | return
18 | }
19 |
20 | def caretModel = editor.caretModel
21 | def document = editor.document
22 |
23 | def line_no = 10
24 | def col_no = 5
25 |
26 | if (document.lineCount >= line_no) {
27 | def lineStartOffset = document.getLineStartOffset(line_no-1)
28 | def lineEndOffset = document.getLineEndOffset(line_no-1)
29 | def targetOffset = Math.min(lineStartOffset + col_no, lineEndOffset)
30 |
31 | caretModel.moveToOffset(targetOffset)
32 | IdeFocusManager.getInstance(project).requestFocus(editor.contentComponent, true)
33 | def scrollingModel = editor.scrollingModel
34 | scrollingModel.scrollToCaret(com.intellij.openapi.editor.ScrollType.RELATIVE)
35 | }
36 | show("scrolled to ${line_no}:${col_no}")
37 | }
38 | if (!isIdeStartup)
39 | show("Loaded 'Set caret and scroll to it'
Use alt+shift+H to run it")
40 |
--------------------------------------------------------------------------------
/licenses/README.md:
--------------------------------------------------------------------------------
1 | ## Third-Party Library Licenses
2 |
3 | This folder contains the licenses for all third-party libraries used in this project. Each license file provides
4 | information about the specific terms under which the library is distributed, including any usage restrictions,
5 | permissions, and limitations.
--------------------------------------------------------------------------------
/licenses/platformdirs-LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2010-202x The platformdirs developers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | python_version = 3.7
3 | check_untyped_defs = true
4 | disallow_untyped_calls = true
5 | disallow_untyped_defs = true
6 | follow_imports = silent
7 | ignore_missing_imports = true
8 | show_column_numbers = true
9 | warn_incomplete_stub = false
10 | warn_redundant_casts = true
11 | warn_unused_ignores = true
12 |
--------------------------------------------------------------------------------
/pip-install-all.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 |
4 | def install_requirements():
5 | subprocess.run(['uv', 'pip', 'install', '-e', '.[all]'], check=True)
6 | subprocess.run(['playwright', 'install-deps', 'chromium'], check=True)
7 |
8 |
9 | if __name__ == "__main__":
10 | install_requirements()
11 |
--------------------------------------------------------------------------------
/preprocessing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/preprocessing/__init__.py
--------------------------------------------------------------------------------
/preprocessing/shoelace/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/preprocessing/shoelace/__init__.py
--------------------------------------------------------------------------------
/pypi_helper_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import subprocess
4 | from pathlib import Path
5 |
6 | from pypi_helper import Pyproject
7 | from pypi_helper import update_minor_version
8 |
9 | pyproject_content = '''
10 | [project]
11 | name = "wwwpy"
12 | version = "0.1.38"
13 | '''
14 |
15 |
16 | def test_Pyproject_properties():
17 | target = Pyproject(pyproject_content)
18 |
19 | assert target.version == '0.1.38'
20 | assert target.content == pyproject_content
21 |
22 |
23 | def test_Pyproject_version_inc_minor():
24 | target = Pyproject(pyproject_content)
25 | target.version_inc_minor()
26 |
27 | assert target.version == '0.1.39'
28 | assert target.content == pyproject_content.replace('0.1.38', '0.1.39')
29 |
30 |
31 | def test_Pyproject_without_args__should_load_pyproject_toml(tmp_path):
32 | target = Pyproject()
33 | assert target.version
34 | assert target.content
35 |
36 |
37 | def test_update_minor_version(tmp_path):
38 | file = tmp_path / "pyproject.toml"
39 | file.write_text(pyproject_content)
40 |
41 | result = update_minor_version(file)
42 | assert result == 'Updated version to 0.1.39'
43 | updated_content = file.read_text()
44 | assert 'version = "0.1.39"' in updated_content
45 |
46 | def test_do_not_add_cr(tmp_path):
47 | file = tmp_path / "pyproject.toml"
48 | file.write_text('[project]\nversion = "0.1.38"\n#comment')
49 |
50 | update_minor_version(file)
51 |
52 | assert file.read_text() == '[project]\nversion = "0.1.39"\n#comment'
53 |
--------------------------------------------------------------------------------
/pypi_inc_minor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import subprocess
4 |
5 | from pypi_helper import uncommitted_changes, update_minor_version, write_build_meta
6 |
7 |
8 | def main():
9 | if uncommitted_changes():
10 | return
11 |
12 | msg = update_minor_version()
13 | if msg:
14 | print('=== committing changes')
15 | write_build_meta()
16 | subprocess.run(['git', 'commit', '-am', msg])
17 |
18 |
19 | if __name__ == '__main__':
20 | main()
21 |
--------------------------------------------------------------------------------
/pypi_upload.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import os
3 | import subprocess
4 | import sys
5 |
6 | from pypi_helper import uncommitted_changes
7 |
8 |
9 | def main():
10 | if uncommitted_changes():
11 | return
12 | # Load environment variables from .env
13 | with open('.env') as f:
14 | for line in f:
15 | line = line.strip()
16 | if not line or line.startswith('#'):
17 | continue
18 | if '=' in line:
19 | key, value = line.split('=', 1)
20 | os.environ[key] = value
21 |
22 | # Remove all files in dist/
23 | dist_files = glob.glob('dist/*')
24 | for file in dist_files:
25 | os.remove(file)
26 |
27 | # Build the package
28 | try:
29 | subprocess.run([sys.executable, '-m', 'build'], check=True)
30 | except subprocess.CalledProcessError as e:
31 | sys.exit(e.returncode)
32 |
33 | # Upload the package to PyPI
34 | dist_files = glob.glob('dist/*')
35 | if dist_files:
36 | subprocess.run([sys.executable, '-m', 'twine', 'upload'] + dist_files, check=True)
37 | subprocess.run(['git', 'push'])
38 | else:
39 | print("No files found in dist/ to upload.")
40 |
41 |
42 | if __name__ == '__main__':
43 | main()
44 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts=--showlocals
3 | asyncio_mode = auto
4 | asyncio_default_fixture_loop_scope = function
5 | log_cli_level=DEBUG
6 |
7 | python_files=test_*.py *_test.py *_fixture.py
8 | log_cli=1
9 | #
10 | ; addopts = --headed
11 | #--slowmo 2000
12 | ; addopts = --assert=plain
13 |
--------------------------------------------------------------------------------
/src/wwwpy/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | from . import _build_meta
3 |
4 | __version__ = _build_meta.__version__
5 | __banner__ = f'wwwpy v{__version__}'
6 | except:
7 | __version__ = 'unknown'
8 | __banner__ = 'wwwpy version unknown'
9 |
10 | __all__ = ['__version__']
11 |
--------------------------------------------------------------------------------
/src/wwwpy/_build_meta.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.1.85"
2 | git_hash_short = "de8c216"
3 | git_hash = "de8c2161a0c53284014bbbc2bd5e92f7087227b7"
4 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/asgi/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/asgi/asgi-compliant.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 1. **Uvicorn** - Fast ASGI server built on `uvloop` and `httptools`.
6 | 2. **Daphne** - ASGI server maintained by the Django project, supports HTTP, HTTP2, and WebSockets.
7 | 3. **Hypercorn** - ASGI server built on `asyncio` and `trio`, supports HTTP/2 and WebSockets.
8 | 4. **Gunicorn with Uvicorn workers** - Flexible WSGI/ASGI server using Uvicorn as workers for ASGI support.
9 | 5. **Mangum** - ASGI adapter for AWS Lambda to deploy ASGI apps on serverless infrastructure.
10 | 6. **Starlette** - While primarily a framework, it can run as an ASGI app and be used with ASGI servers like Uvicorn.
11 | 7. **Quart** - An ASGI-compatible framework based on Flask, with a built-in server for ASGI.
12 | 8. **ASGIRef** - Lightweight, spec-compliant ASGI server.
13 | 9. **Litestar (formerly Starlite)** - ASGI server and web framework optimized for performance.
14 | 10. **FastAPI** - Although primarily a framework, FastAPI apps run on ASGI, typically with Uvicorn or Hypercorn.
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/asgi/favicon.png
--------------------------------------------------------------------------------
/src/wwwpy/asgi/favicon.txt:
--------------------------------------------------------------------------------
1 | w cute at
2 | https://icons8.com/icon/ZUpyBfSaJr8a/w-cute
3 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WebSocket Echo Client
7 |
13 |
14 |
15 | WebSocket Echo Test
16 |
17 |
18 |
19 |
20 | Log:
21 |
22 |
23 |
66 |
67 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/main_daphne.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | # uv pip install daphne
4 | subprocess.run(['daphne', 'wwwpy.asgi.echo_handler:app'])
5 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/main_granian.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | from pathlib import Path
3 |
4 | wd = Path(__file__).parent.parent.parent
5 |
6 | # uv pip install granian
7 | subprocess.run(['granian', '--interface', 'asgi', 'wwwpy.asgi.echo_handler:app'], cwd=wd)
8 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/main_hypercorn.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from hypercorn.config import Config
3 | from hypercorn.asyncio import serve
4 |
5 | from echo_handler import app
6 |
7 | # uv pip install hypercorn
8 | config = Config()
9 | config.bind = '0.0.0.0:8000'
10 | asyncio.run(serve(app, config))
11 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/main_tornado.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | # uv pip install tornado
4 |
5 | import tornado.ioloop
6 | import tornado.web
7 | import tornado_asgi_handler
8 | import echo_handler
9 |
10 | application = tornado.web.Application([
11 | (r".*", tornado_asgi_handler.ASGIHandler, dict(asgi_app=echo_handler.app))
12 | ])
13 | application.listen(8000)
14 |
15 | try:
16 | tornado.ioloop.IOLoop.current().start()
17 | except KeyboardInterrupt as err:
18 | print("Server stopped")
19 |
--------------------------------------------------------------------------------
/src/wwwpy/asgi/main_uvicorn.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 |
3 | # uv pip install 'uvicorn[standard]'
4 | subprocess.run(['uvicorn', 'wwwpy.asgi.echo_handler:app', '--reload'])
5 |
--------------------------------------------------------------------------------
/src/wwwpy/base_conf.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | _INITIAL = set(globals())
4 |
5 | PLAYWRIGHT_PATCH_TIMEOUT_MILLIS = 45000
6 | PLAYWRIGHT_HEADFUL = False
7 |
8 | _NEW = set(globals()) - _INITIAL
9 | _NEW.remove('_INITIAL')
10 | try:
11 | import wwwpy_user_conf
12 |
13 | # list all the local custom variables and copy them (if they exists from wwwpy_user_conf)
14 | for key in _NEW:
15 | if hasattr(wwwpy_user_conf, key):
16 | value = getattr(wwwpy_user_conf, key)
17 | globals()[key] = value
18 | else:
19 | env_value = os.environ.get(key)
20 | if env_value is not None:
21 | globals()[key] = env_value
22 |
23 | except ModuleNotFoundError:
24 | import logging
25 |
26 | logger = logging.getLogger(__name__)
27 | logger.info("No local wwwpy_user_conf.py file found. Using default values.")
28 |
--------------------------------------------------------------------------------
/src/wwwpy/common/__init__.py:
--------------------------------------------------------------------------------
1 | _no_remote_infrastructure_found_text = """
2 | Import of package "remote" failed. Unable to bootstrap the application."""
3 |
4 | # language=html
5 | _remote_module_not_found_html = """
6 | Warning, no module 'remote' was found.
7 |
8 |
9 | This may be because the running directory is not a valid wwwpy project directory.
10 |
11 |
12 | If you want to create a new project from the quickstarter, be sure to run from an empty directory.
13 |
14 | Read for more information about the
15 |
16 | quickstarter and a
17 |
18 | general project structure.
19 | """
20 | _warn = '\033[33m[WARN]\033[0m'
21 | _remote_module_not_found_console = f"""{_warn} The current project folder is not empty: $[directory]
22 | {_warn} It does not contain a `remote` package, which is required for a wwwpy project.
23 | If you want to create a new project from the quickstarter, be sure to run from an empty directory.
24 | Read for more information here: https://github.com/wwwpy-labs/wwwpy/blob/main/docs/introduction.md"""
25 |
--------------------------------------------------------------------------------
/src/wwwpy/common/asynclib.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from typing import Union, Coroutine, Any
3 |
4 | # todo evaluate if best to use Awaitable instead of Coroutine
5 | OptionalCoroutine = Union[None, Coroutine[Any, Any, None]]
6 | _task_set = set()
7 |
8 |
9 | def create_task_safe(coro):
10 | """Create an asyncio task and keep a reference to prevent garbage collection.
11 |
12 | See https://stackoverflow.com/questions/71938799/python-asyncio-create-task-really-need-to-keep-a-reference
13 | """
14 | task = asyncio.create_task(coro)
15 | _task_set.add(task)
16 | task.add_done_callback(_task_set.discard)
17 | return task
18 |
--------------------------------------------------------------------------------
/src/wwwpy/common/collectionlib.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Callable, TypeVar, Any, Generic, Collection
4 |
5 | T = TypeVar('T')
6 | K = TypeVar('K')
7 |
8 |
9 | class ListMap(list[T]):
10 | def __init__(self, args: Collection[T] = (), key_func: Callable[[T], K] = None):
11 | super().__init__(args)
12 | if key_func is not None:
13 | self._key = key_func
14 | self._map = {self._key(item): item for item in self}
15 |
16 | # __add__ = _modify_method(list.__add__, takes_list=True)
17 | # __iadd__ = _modify_method(list.__iadd__, takes_list=True)
18 | # __setitem__ = _modify_method(list.__setitem__, 1)
19 | def _key(self, item: T) -> K:
20 | return item
21 |
22 | def append(self, value: T):
23 | self._map[self._key(value)] = value
24 | super().append(value)
25 |
26 | def insert(self, index: int, value: T):
27 | self._map[self._key(value)] = value
28 | super().insert(index, value)
29 |
30 | def extend(self, values: Collection[T]):
31 | for value in values:
32 | self._map[self._key(value)] = value
33 | super().extend(values)
34 |
35 | def get(self, key: K) -> T | None:
36 | """Return the item with the given key or None if it does not exist."""
37 | return self._map.get(key)
38 |
--------------------------------------------------------------------------------
/src/wwwpy/common/databind/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/databind/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/databind/databind.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass
4 | from typing import Callable, List
5 |
6 | from wwwpy.common.property_monitor import Monitor, PropertyChanged, set_origin, monitor_changes, Monitorable, \
7 | get_monitor_or_create
8 |
9 |
10 | @dataclass
11 | class TargetOriginEvent:
12 | origin_target: TargetAdapter
13 | value: any
14 |
15 |
16 | # todo rename to BindingTarget or Bindable or BindableTarget?
17 | class TargetAdapter(Monitorable):
18 |
19 | def __init__(self):
20 | super().__init__()
21 |
22 | def set_target_value(self, value):
23 | pass
24 |
25 | def get_target_value(self):
26 | pass
27 |
28 |
29 | class Binding:
30 |
31 | def __init__(self, source, attr_name, target_adapter: TargetAdapter):
32 | super().__init__()
33 | self.source = source
34 | self.attr_name = attr_name
35 | self.target_adapter = target_adapter
36 | target_adapter.monitor_object.listeners.append(self._on_target_changes)
37 | get_monitor_or_create(source).add_attribute_listener(attr_name, self._on_source_changes)
38 |
39 | def apply_binding(self):
40 | self.target_adapter.set_target_value(getattr(self.source, self.attr_name))
41 |
42 | def _on_target_changes(self, events: List[PropertyChanged]):
43 | event = events[-1]
44 | with set_origin(self.source, self):
45 | setattr(self.source, self.attr_name, event.new_value)
46 |
47 | def _on_source_changes(self, events: List[PropertyChanged]):
48 | event = events[-1] # todo we should filter only for the attr_name!
49 | if event.origin == self:
50 | return
51 | with set_origin(self.target_adapter, self):
52 | self.target_adapter.set_target_value(event.new_value)
53 |
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/__init__.py:
--------------------------------------------------------------------------------
1 | pypi_packages = ['libcst==1.4.0', 'rope==1.13.0']
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/canvas_selection.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from dataclasses import dataclass
5 |
6 | from wwwpy.common.designer.locator_lib import Locator
7 | from wwwpy.common.type_listener import TypeListeners
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | @dataclass
13 | class CanvasSelectionChangeEvent:
14 | old: Locator | None
15 | new: Locator | None
16 |
17 |
18 | class CanvasSelection:
19 | """At the time of writing, this class is listened by the old infrastructure, but
20 | it is not updated by it. Who writes it is the new infrastructure.
21 | """
22 | current_selection: Locator | None
23 | """It looks like the ElementPath should have Origin.live"""
24 |
25 | def __init__(self):
26 | self._current_selection = None
27 | self.on_change: TypeListeners[CanvasSelectionChangeEvent] = TypeListeners(CanvasSelectionChangeEvent)
28 |
29 | @property
30 | def current_selection(self) -> Locator | None:
31 | return self._current_selection
32 |
33 | @current_selection.setter
34 | def current_selection(self, value: Locator | None):
35 | old = self._current_selection
36 | self._current_selection = value
37 | event = CanvasSelectionChangeEvent(old, value)
38 | logger.debug(f'CanvasSelection: {old} -> {value}')
39 | self.on_change.notify(event)
40 |
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/class_path.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass
4 |
5 |
6 | @dataclass()
7 | class ClassPath:
8 | class_module: str
9 | """The module name of the Component."""
10 | class_name: str
11 | """The class name of the Component."""
12 |
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/log_emit.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import warnings
3 | from typing import Callable
4 |
5 |
6 | class _CustomHandler(logging.Handler):
7 | def __init__(self, callback: Callable[[str], None]):
8 | super().__init__()
9 | self._emit = callback
10 |
11 | def emit(self, record: logging.LogRecord):
12 | log_entry = self.format(record)
13 | self._emit(log_entry)
14 |
15 |
16 | def add_once(emit: Callable[[str], None]):
17 | logging.getLogger('tornado.access').setLevel(logging.ERROR)
18 |
19 | for log_name in ['common', 'remote', 'server']:
20 | logging.getLogger(log_name).setLevel(logging.DEBUG)
21 |
22 | root = logging.getLogger()
23 |
24 | for handler in root.handlers:
25 | if isinstance(handler, _CustomHandler):
26 | return
27 | from wwwpy.common.detect import is_pyodide
28 | # side = 'R' if is_pyodide() else 'S'
29 | side = '🌎' if is_pyodide() else '🏛'
30 | formatter = logging.Formatter(f'%(asctime)s {side} %(levelname).1s %(name)s:%(lineno)d - %(message)s')
31 | custom_handler = _CustomHandler(emit)
32 | custom_handler.setFormatter(formatter)
33 | root.addHandler(custom_handler)
34 |
35 |
36 | _original_showwarning = None
37 |
38 |
39 | def _custom_showwarning(message, category, filename, lineno, file=None, line=None):
40 | if _original_showwarning:
41 | _original_showwarning(message, category, filename, lineno, file, line)
42 | log_entry = f'{category.__name__}: {message} ({filename}:{lineno})'
43 | logging.getLogger('common').warning(log_entry)
44 |
45 |
46 | def warning_to_log():
47 | global _original_showwarning
48 | if _original_showwarning:
49 | return
50 | _original_showwarning = warnings.showwarning
51 | warnings.showwarning = _custom_showwarning
52 |
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/new_component.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from pathlib import Path
5 | from typing import List
6 |
7 | from wwwpy.common import modlib
8 |
9 | import logging
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def is_component(path: Path):
15 | return (path.is_file() and path.suffix == '.py' and
16 | path.name.startswith('component'))
17 |
18 |
19 | def next_name_for(current_files: List[str]) -> tuple[int, str]:
20 | files = set(current_files)
21 | for i in range(1, sys.maxsize):
22 | name = f'component{i}.py'
23 | if name not in files:
24 | return i, name
25 | raise ValueError('Really?')
26 |
27 |
28 | def next_component_path(folder: Path | None = None) -> tuple[int, Path] | None:
29 | if folder is None:
30 | folder = modlib._find_package_directory('remote')
31 | if folder is None:
32 | logger.error('remote package not found')
33 | return None
34 | index, name = next_name_for([d.name for d in folder.iterdir() if is_component(d)])
35 | file = folder / name
36 | assert not file.exists()
37 | return index, file
38 |
39 |
40 | def main():
41 | add()
42 |
43 |
44 | def add(folder: Path | None = None) -> Path | None:
45 | if folder is None:
46 | folder = modlib._find_package_directory('remote')
47 | if folder is None:
48 | logger.error('remote package not found')
49 | return None
50 |
51 | index, name = next_name_for([d.name for d in folder.iterdir() if is_component(d)])
52 | file = folder / name
53 | assert not file.exists()
54 | content = f'''import wwwpy.remote.component as wpc
55 | import js
56 |
57 | import logging
58 |
59 | logger = logging.getLogger(__name__)
60 |
61 | class Component{index}(wpc.Component, tag_name='component-{index}'):
62 | def init_component(self):
63 | # language=html
64 | self.element.innerHTML = """
65 | component-{index}
"""
66 | '''
67 | file.write_text(content)
68 | return file
69 |
70 |
71 | if __name__ == '__main__':
72 | main()
73 |
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/packaging/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/designer/packaging/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/packaging/packages.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class Package:
7 | name: str
8 | version: str
9 |
10 |
11 | @dataclass(frozen=True)
12 | class PackageSpecification:
13 | name: str
14 | version_specs: str | None = None
15 | """Version specifications for the package or None for any version. E.g. '==1.0.0' or '>=1.0.0'
16 |
17 | See https://packaging.python.org/en/latest/specifications/version-specifiers/
18 | """
19 |
20 | def build_installation_string(self) -> list[str]:
21 | if self.version_specs is None:
22 | return [self.name]
23 | return [f"{self.name}{self.version_specs}"]
24 |
25 |
26 | @dataclass(frozen=True)
27 | class PackageRequest:
28 | install: list[PackageSpecification]
29 |
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/designer/ui/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/designer/ui/rect_readonly.py:
--------------------------------------------------------------------------------
1 | from typing import Protocol
2 |
3 |
4 | class RectReadOnly(Protocol):
5 | """This is intended to be used in common to handle js.DOMRectReadOnly"""
6 | x: float
7 | y: float
8 | width: float
9 | height: float
10 | top: float
11 | right: float
12 | bottom: float
13 | left: float
14 |
15 | def toJSON(self) -> object: ...
16 |
17 |
18 | def rect_xy_center(rect: RectReadOnly) -> tuple[float, float]:
19 | """Get the center x and y coordinates of the given rect."""
20 | x = rect.x + rect.width / 2
21 | y = rect.y + rect.height / 2
22 | return x, y
23 |
24 |
25 | def rect_to_py(rect: RectReadOnly) -> RectReadOnly:
26 | from . import rect_readonly_py
27 | return rect_readonly_py.RectReadOnlyPy(rect)
28 |
--------------------------------------------------------------------------------
/src/wwwpy/common/detect.py:
--------------------------------------------------------------------------------
1 | def is_pyodide() -> bool:
2 | try:
3 | import pyodide
4 | return True
5 | except ImportError:
6 | return False
7 |
--------------------------------------------------------------------------------
/src/wwwpy/common/escapelib.py:
--------------------------------------------------------------------------------
1 | # Create a translation table for the specified characters
2 | escape_table = str.maketrans({
3 | '\r': '\\r', # Carriage return -> \r
4 | '\n': '\\n', # Newline (line feed) -> \n
5 | '\t': '\\t', # Tab -> \t
6 | '\\': '\\\\' # Backslash -> \\
7 | })
8 |
9 |
10 | def escape_string(s: str) -> str:
11 | return s.translate(escape_table)
12 |
13 |
14 | def unescape_string(s: str) -> str:
15 | escaped_bytes = s.encode('ascii', 'backslashreplace')
16 | return escaped_bytes.decode('unicode_escape')
17 |
--------------------------------------------------------------------------------
/src/wwwpy/common/event_observer.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 |
4 | class EventObserver:
5 | _last_event: datetime
6 | _now: callable
7 |
8 | def __init__(self, min_delay_millis, time_provider=None):
9 | self._min_delay_millis = min_delay_millis
10 | if time_provider is None:
11 | time_provider = datetime.utcnow
12 | self._now = time_provider
13 | self._update()
14 |
15 | def _update(self):
16 | self._last_event = self._now()
17 |
18 | def event_happened(self):
19 | self._update()
20 |
21 | def is_stable(self):
22 | if self._min_delay_millis == 0:
23 | return True
24 | now = self._now()
25 | diff: timedelta = now - self._last_event
26 | return diff.total_seconds() * 1000 > self._min_delay_millis
27 |
--------------------------------------------------------------------------------
/src/wwwpy/common/exitlib.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import weakref
3 | from typing import Callable
4 |
5 |
6 | class _Obj:
7 | pass
8 |
9 |
10 | def on_exit(callback: Callable[[], None]) -> None:
11 | frame = sys._getframe(1)
12 | obj = _Obj()
13 | frame.f_locals[obj] = obj
14 | weakref.finalize(obj, callback)
15 |
--------------------------------------------------------------------------------
/src/wwwpy/common/fetch.py:
--------------------------------------------------------------------------------
1 | async def async_fetch_str(url: str, method: str = 'GET', data: str = '') -> str:
2 | pass
3 |
4 |
5 | try:
6 | from wwwpy.remote.fetch import async_fetch_str
7 | except ImportError or ModuleNotFoundError:
8 | from wwwpy.server.fetch import async_fetch_str
9 |
--------------------------------------------------------------------------------
/src/wwwpy/common/fetch_debug.py:
--------------------------------------------------------------------------------
1 | def fetch_debug(url, method, data) -> str:
2 | return f'method={method} len(data)={len(data)} url=`{url}` data=`{data[:100]}`'
3 |
--------------------------------------------------------------------------------
/src/wwwpy/common/filesystem/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/filesystem/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/filesystem/sync/__init__.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | from pathlib import Path
3 | from typing import List, Protocol, Any
4 |
5 | from wwwpy.common.filesystem.sync.event import Event
6 |
7 |
8 | def new_tmp_path() -> Path:
9 | return Path(tempfile.mkdtemp(prefix='debounce-tmp-path-'))
10 |
11 | class Sync(Protocol):
12 |
13 | @staticmethod
14 | def sync_source(source: Path, events: List[Event]) -> List[Any]:
15 | """Could be called 'sync_produce'. This part has the following input:
16 | - a reference to the root of the monitored source filesystem
17 | - a list of filesystem change events
18 |
19 |
20 | The output is:
21 | - a list of filesystem changes that can be serialized.
22 | - such list can be an aggregated list of changes (e.g., a file was created and then modified, the two events can be aggregated into a single event)
23 |
24 | """
25 |
26 | @staticmethod
27 | def sync_target(target: Path, changes: List[Any]) -> None:
28 | """Could be called 'sync_apply'. This part has the following input:
29 | - a reference to the root of the target filesystem
30 | - a list of filesystem aggregated change events
31 |
32 | The output is:
33 | - the target filesystem is updated to reflect the changes
34 | """
35 |
36 | @staticmethod
37 | def sync_init(source: Path) -> List[Any]:
38 | """This part has the following input:
39 | - a reference to the root of the monitored source filesystem
40 |
41 | The output is:
42 | - a list of filesystem aggregated change events that reflect the current state of the source filesystem
43 | """
44 |
--------------------------------------------------------------------------------
/src/wwwpy/common/filesystem/sync/event_rebase.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 | from typing import List, Set
5 |
6 | from wwwpy.common.filesystem import sync
7 |
8 |
9 | def filter_by_directory(events: List[sync.Event], directory_set: Set[str | Path]) -> List[sync.Event]:
10 | """Rebase events that falls in directory_set.
11 | It will retain the original path because the events will be used to send sync content to the remote;
12 | so the files still need to be accessible."""
13 | new_events = []
14 | path_set = {Path(d) for d in directory_set}
15 |
16 | def accept(event: sync.Event) -> bool:
17 | src_path = Path(event.src_path)
18 | for path in path_set:
19 | if src_path.is_relative_to(path):
20 | return True
21 | return False
22 |
23 | for event in events:
24 | if accept(event):
25 | new_events.append(event)
26 | return new_events
27 |
--------------------------------------------------------------------------------
/src/wwwpy/common/filesystem/sync/sync_delta2.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import List, Any
3 |
4 | from wwwpy.common.rpc import serialization
5 | from wwwpy.common.filesystem.sync import Event
6 | from wwwpy.common.filesystem.sync import event_invert_apply
7 |
8 |
9 | def sync_source(source: Path, events: List[Event]) -> List[Any]:
10 | events_inverted = event_invert_apply.events_invert(source, events)
11 | ser = serialization.serialize(events_inverted, List[Event])
12 | return ser
13 |
14 |
15 | def sync_target(target_root: Path, changes: List[Any]) -> None:
16 | events = serialization.deserialize(changes, List[Event])
17 | event_invert_apply.events_apply(target_root, events)
18 |
19 |
20 | def sync_init(source: Path) -> List[Any]:
21 | events = event_invert_apply.events_init(source)
22 | ser = serialization.serialize(events, List[Event])
23 | return ser
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/wwwpy/common/indent.py:
--------------------------------------------------------------------------------
1 | import textwrap
2 |
3 |
4 | def indent_code(code: str, times=1) -> str:
5 | return textwrap.indent(code, ' ' * (4 * times))
6 |
--------------------------------------------------------------------------------
/src/wwwpy/common/iterlib.py:
--------------------------------------------------------------------------------
1 | from typing import Iterable, TypeVar, Generic, Callable, Iterator
2 |
3 | _T = TypeVar('_T')
4 |
5 |
6 | class repeatable_chain(Generic[_T]):
7 | def __init__(self, *iterables: Iterable[_T]):
8 | self._iterables = iterables
9 |
10 | def __iter__(self) -> Iterator[_T]:
11 | def it() -> Iterator[_T]:
12 | for iterable in self._iterables:
13 | yield from iterable
14 |
15 | return it()
16 |
17 |
18 | class CallableToIterable(Generic[_T]):
19 | def __init__(self, iterator_factory: Callable[[], Iterator[_T]]):
20 | self.iterator_factory = iterator_factory
21 |
22 | def __iter__(self) -> Iterator[_T]:
23 | try:
24 | return iter_catching(self.iterator_factory())
25 | except:
26 | import traceback
27 | traceback.print_exc()
28 | return iter([])
29 |
30 |
31 | def iter_catching(gen):
32 | while True:
33 | try:
34 | yield next(gen)
35 | except StopIteration:
36 | break
37 | except Exception as e:
38 | import traceback
39 | traceback.print_exc()
40 |
--------------------------------------------------------------------------------
/src/wwwpy/common/loglib.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | def set_log_level(log_level: dict[str, str]):
5 | tr = translate_names(log_level)
6 | for module, level in tr.items():
7 | logging.getLogger(module).setLevel(level)
8 |
9 |
10 | def translate_names(log_level):
11 | translated = {}
12 | for module, level in log_level.items():
13 | name = logging.getLevelName(level)
14 | if isinstance(name, int):
15 | translated[module] = name
16 | return translated
17 |
--------------------------------------------------------------------------------
/src/wwwpy/common/modlib.py:
--------------------------------------------------------------------------------
1 | """Module library for finding module paths and roots."""
2 | from __future__ import annotations
3 |
4 | import os
5 | import sys
6 | from pathlib import Path
7 |
8 | import logging
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 | def _find_module_path(module_name: str) -> Path | None:
13 | """Finds the path of a module without loading it."""
14 | parts = module_name.split('.')
15 | for sys_path in map(Path, sys.path):
16 | module_path = sys_path.joinpath(*parts)
17 | py_file = module_path.with_suffix('.py')
18 | init_file = module_path / '__init__.py'
19 |
20 | if py_file.is_file():
21 | return py_file
22 | if module_path.is_dir() and init_file.is_file():
23 | return init_file
24 |
25 | return None
26 |
27 | def _find_package_directory(package_name) -> Path | None:
28 | """Finds the directory of a package without loading it."""
29 | path = _find_module_path(package_name)
30 | if not path:
31 | return None
32 | if path.name != '__init__.py':
33 | raise ValueError(f'package {package_name} is not a package')
34 | return path.parent
35 |
36 |
37 | def _find_module_root(fqn, full_path):
38 | parts = fqn.split('.')
39 | relative_path = os.path.join(*parts[:-1]) + '.py'
40 | full_path = os.path.abspath(full_path)
41 | index = full_path.rfind(relative_path)
42 | if index == -1:
43 | return None
44 | return full_path[index:]
45 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/basic/readme.txt:
--------------------------------------------------------------------------------
1 | Basic setup for a new project
2 |
3 | It contains just a simple Component with almost no content
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/basic/remote/__init__.py:
--------------------------------------------------------------------------------
1 | from js import document
2 |
3 |
4 | async def main():
5 | from wwwpy.remote import shoelace
6 | shoelace.setup_shoelace()
7 |
8 | from . import component1 # for component registration
9 | document.body.innerHTML = ''
10 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/basic/remote/component1.py:
--------------------------------------------------------------------------------
1 | import wwwpy.remote.component as wpc
2 | import js
3 |
4 | import logging
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | class Component1(wpc.Component, tag_name='component-1'):
10 |
11 | def init_component(self):
12 | # language=html
13 | self.element.innerHTML = """
14 | component-1
15 | """
16 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/chat/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/quickstart/chat/common/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/chat/common/name.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | adjectives = [
4 | 'adaptive', 'agile', 'ambitious', 'analytical', 'bold', 'brilliant', 'clever', 'crazy', 'creative', 'curious',
5 | 'daring', 'determined', 'dynamic', 'efficient', 'energetic', 'fearless', 'focused', 'innovative', 'insightful',
6 | 'inspired', 'intuitive', 'masterful', 'mighty', 'nimble', 'passionate', 'proactive', 'quick', 'reliable',
7 | 'resourceful', 'savvy', 'sharp', 'skillful', 'sleek', 'smart', 'strategic', 'talented', 'tenacious',
8 | 'versatile', 'visionary', 'witty'
9 | ]
10 |
11 | roles = [
12 | 'ace', 'analyst', 'architect', 'artisan', 'builder', 'champion', 'coder', 'connoisseur', 'creator', 'debugger',
13 | 'designer', 'developer', 'dynamo', 'engineer', 'expert', 'genius', 'guru', 'innovator', 'luminary', 'maestro',
14 | 'maintainer', 'manager', 'master', 'maverick', 'ninja', 'operator', 'pioneer', 'prodigy', 'programmer', 'sage',
15 | 'specialist', 'strategist', 'technician', 'trailblazer', 'tuner', 'virtuoso', 'visionary', 'whiz', 'wizard',
16 | 'wrangler'
17 | ]
18 |
19 |
20 | def generate_name():
21 | return f"{random.choice(adjectives)}-{random.choice(roles)}"
22 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/chat/readme.txt:
--------------------------------------------------------------------------------
1 | A simple chat application
2 |
3 | It shows how to send messages from the browser (remote) to the server (local).
4 | It also shows how to send messages from the server to all the browsers currently connected.
5 |
6 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/chat/remote/__init__.py:
--------------------------------------------------------------------------------
1 | from js import document
2 |
3 |
4 | async def main():
5 | from wwwpy.remote import shoelace
6 | shoelace.setup_shoelace()
7 |
8 | from . import component1 # for component registration
9 | document.body.innerHTML = ''
10 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/chat/remote/rpc.py:
--------------------------------------------------------------------------------
1 | messages_listeners = []
2 |
3 |
4 | class Rpc:
5 | def new_message(self, msg: str):
6 | for listener in messages_listeners:
7 | listener(msg)
8 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/chat/server/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/quickstart/chat/server/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/chat/server/rpc.py:
--------------------------------------------------------------------------------
1 | from remote import rpc
2 |
3 | from wwwpy.server.convention import default_project
4 |
5 |
6 | async def send_message_to_all(msg: str) -> str:
7 | for client in default_project().websocket_pool.clients:
8 | client.rpc(rpc.Rpc).new_message(msg)
9 | return 'done'
10 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/markdown/readme.txt:
--------------------------------------------------------------------------------
1 | Render markdown as you type
2 |
3 | This component demonstrates how to build an interactive markdown renderer using the markdown PyPI package.
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/markdown/remote/__init__.py:
--------------------------------------------------------------------------------
1 | from js import document
2 |
3 |
4 | async def main():
5 | from wwwpy.remote import shoelace
6 | shoelace.setup_shoelace()
7 |
8 | from . import component1 # for component registration
9 | document.body.innerHTML = ''
10 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/markdown/remote/component1.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import wwwpy.remote.component as wpc
4 | import js
5 |
6 | import logging
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class Component1(wpc.Component, tag_name='component-1'):
12 | textarea1: js.HTMLTextAreaElement = wpc.element()
13 | div1: js.HTMLDivElement = wpc.element()
14 | title: js.HTMLElement = wpc.element()
15 |
16 | def init_component(self):
17 | # language=html
18 | self.element.innerHTML = """
19 | Loading...
20 |
21 |
33 |
34 |
35 |
36 |
37 |
38 | """
39 | asyncio.ensure_future(self.after_init())
40 |
41 | async def after_init(self):
42 | logger.info('Component1 after_init')
43 | import micropip
44 | await micropip.install('markdown')
45 | import markdown
46 | # language=markdown
47 | self.title.innerHTML = markdown.markdown(f"""
48 | # Component1
49 | - This is a simple component that uses a textarea and a div to render markdown.
50 | - This text itself is rendered using markdown.
51 | """)
52 | self.textarea1.value = js.localStorage.getItem('textarea1') or 'Write your _markdown_ here ...'
53 | self._render_markdown()
54 |
55 | async def textarea1__input(self, event):
56 | js.localStorage.setItem('textarea1', self.textarea1.value)
57 | self._render_markdown()
58 |
59 | def _render_markdown(self):
60 | import markdown
61 | self.div1.innerHTML = markdown.markdown(self.textarea1.value)
62 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/upload/.gitignore:
--------------------------------------------------------------------------------
1 | uploads
2 | .idea
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/upload/readme.txt:
--------------------------------------------------------------------------------
1 | A simple component to upload files to the server
2 |
3 | It shows how to upload files from the browser (remote) to the server python process (server).
4 | It supports multiple file uploads with progress bars.
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/upload/remote/__init__.py:
--------------------------------------------------------------------------------
1 | from js import document
2 |
3 |
4 | async def main():
5 | from wwwpy.remote import shoelace
6 | shoelace.setup_shoelace()
7 |
8 | from . import component1 # for component registration
9 | from . import upload_component # for component registration
10 | document.body.innerHTML = ''
11 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/upload/remote/component1.py:
--------------------------------------------------------------------------------
1 | import js
2 | import logging
3 |
4 | import wwwpy.remote.component as wpc
5 |
6 | from .upload_component import UploadComponent
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class Component1(wpc.Component, tag_name='component-1'):
12 | multiple_checkbox: js.HTMLInputElement = wpc.element()
13 | upload1: UploadComponent = wpc.element()
14 |
15 | def init_component(self):
16 | # language=html
17 | self.element.innerHTML = """
18 |
19 |
20 |
Component1 in component1.py
21 |
The files are uploaded in the project root 'uploads' folder. To change this behaviour see file server/rpc.py
22 |
25 |
The component below the line is defined in upload_component.py
26 |
27 |
28 |
29 | """
30 | self.multiple_checkbox.checked = self.upload1.multiple
31 |
32 | async def multiple_checkbox__input(self, event):
33 | js.console.log('handler multiple_checkbox__input event =', event)
34 | self.upload1.multiple = self.multiple_checkbox.checked
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/upload/server/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/quickstart/upload/server/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/quickstart/upload/server/rpc.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import logging
3 | from pathlib import Path
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 |
8 | async def upload_init(name: str, size: int):
9 | file = _resolve_file(name)
10 | file.unlink(missing_ok=True)
11 | file.touch()
12 | logger.info(f'upload_init name={name} size={size}')
13 | return None
14 |
15 |
16 | async def upload_append(name: str, b64str: str):
17 | logger.info(f'upload_append name={name} len(b64str)={len(b64str)}')
18 | file = _resolve_file(name)
19 | bytes_append = base64.b64decode(b64str)
20 | with file.open('ab') as f:
21 | f.write(bytes_append)
22 | return None
23 |
24 |
25 | async def upload_abort(name: str):
26 | logger.info(f'upload_abort name={name}')
27 | file = _resolve_file(name)
28 | file.unlink(missing_ok=True)
29 | return None
30 |
31 |
32 | def _resolve_file(name: str) -> Path:
33 | folder = Path(__file__).parent.parent / 'uploads'
34 | candidate = folder / name
35 | # security check: candidate is inside the folder?
36 | if not candidate.resolve().is_relative_to(folder.resolve()):
37 | raise ValueError(f'Invalid path: {candidate}')
38 | candidate.parent.mkdir(parents=True, exist_ok=True)
39 | return candidate
40 |
--------------------------------------------------------------------------------
/src/wwwpy/common/reloader.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | import sys
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | def reload(module):
10 | import importlib
11 | # importlib.invalidate_caches()
12 | return importlib.reload(module)
13 |
14 |
15 | def unload_path(path: str, skip_wwwpy: bool = False):
16 | def accept(module):
17 | try:
18 | module_file = getattr(module, '__file__', None)
19 | if module_file:
20 | return module_file.startswith(path) and module_file != __file__
21 | module_path = getattr(module, '__path__', None)
22 | if module_path:
23 | acc = any(p.startswith(path) for p in module_path)
24 | if acc and not all(p.startswith(path) for p in module_path):
25 | logger.warning(f'hot-reload: module `{module}` has mixed paths: {module_path}')
26 | return acc
27 | except:
28 | return False
29 |
30 | names = [name for name, module in sys.modules.items() if accept(module)]
31 |
32 | for name in names:
33 | if skip_wwwpy and name.startswith('wwwpy.') or name == 'wwwpy':
34 | logger.debug(f'hot-reload: skip module `{name}`')
35 | else:
36 | logger.debug(f'hot-reload: unload module `{name}`')
37 | del (sys.modules[name])
38 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/rpc/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc/custom_loader.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import importlib.abc
3 | import importlib.util
4 | import sys
5 | import ast
6 | from typing import Set
7 |
8 | from wwwpy.common.rpc import func_registry
9 | from ast import Module, FunctionDef, AsyncFunctionDef, ClassDef
10 |
11 |
12 | class CustomLoader(importlib.abc.Loader):
13 | def __init__(self, loader):
14 | self.loader = loader
15 |
16 | def create_module(self, spec):
17 | return None
18 |
19 | def exec_module(self, module):
20 | module_name = module.__name__
21 | source = self.loader.get_source(module_name)
22 | proxy_source = func_registry.source_to_proxy(module_name, source)
23 | code = compile(proxy_source, module.__file__, 'exec')
24 | exec(code, module.__dict__)
25 |
26 |
27 | class CustomFinder(importlib.abc.MetaPathFinder):
28 | """It intercepts the packages specified and rewrites them as a proxy-to-remote.
29 | The goal is to seamlessly invoke remote functions from the server"""
30 |
31 | def __init__(self, packages_name: Set[str], custom_loader=CustomLoader):
32 | self.custom_loader = custom_loader
33 | self.packages_name = packages_name
34 | super().__init__()
35 |
36 | def find_spec(self, fullname, path, target=None):
37 | if fullname in self.packages_name:
38 | # Temporarily remove this finder from sys.meta_path to avoid recursion. Remove also pytest rewriter hook
39 | orig = sys.meta_path.copy()
40 | sys.meta_path = [f for f in sys.meta_path if not isinstance(f, CustomFinder)]
41 | sys.meta_path = [f for f in sys.meta_path if f.__class__.__name__ != 'AssertionRewritingHook']
42 | try:
43 | spec = importlib.util.find_spec(fullname)
44 | finally:
45 | # Reinsert this finder back into sys.meta_path
46 | sys.meta_path = orig
47 |
48 | if spec:
49 | spec.loader = self.custom_loader(spec.loader)
50 | return spec
51 | return None
52 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc/hibrid_dispatcher.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from wwwpy.common.rpc.serializer import RpcRequest, RpcResponse
6 | from wwwpy.common.rpc.v2.dispatcher import Dispatcher
7 | from wwwpy.exceptions import RemoteException
8 |
9 |
10 | # todo cleanup, this is 'mostly' used by the remote for the remote-to-server rpc(s)
11 | # but it's also used by the server tests. This should take care mostly about serialization/deserialization
12 | # and error handling, so we may completely abstract the transport (urllib, browser fetch etc etc)
13 | # and remove old tests and do new one for the new version
14 | # There are two counterparts to the dispatch, it's RpcRoute.dispatch() and setup_websocket().message()
15 | class HybridDispatcher(Dispatcher):
16 | def __init__(self, module_name: str, rpc_url: str):
17 | self.rpc_url = rpc_url
18 | from wwwpy.common.fetch import async_fetch_str
19 | self.fetch = async_fetch_str
20 | self.module_name = module_name
21 |
22 | async def dispatch_async(self, func_name: str, *args) -> Any:
23 | rpc_request = RpcRequest.to_json(self.module_name, func_name, *args)
24 | json_response = await self.fetch(self.rpc_url, method='POST', data=rpc_request)
25 | response = RpcResponse.from_json(json_response)
26 | ex = response.exception
27 | if ex is not None and ex != '':
28 | raise RemoteException(ex)
29 | return response.result
30 |
31 | def dispatch_sync(self, func_name: str, *args) -> Any:
32 | rpc_request = RpcRequest.to_json(self.module_name, func_name, *args)
33 | import js
34 | xhr = js.XMLHttpRequest.new()
35 | xhr.open('POST', self.rpc_url, False)
36 | xhr.setRequestHeader('Content-Type', 'application/json')
37 | xhr.send(rpc_request)
38 | json_response = xhr.responseText
39 | response = RpcResponse.from_json(json_response)
40 | ex = response.exception
41 | if ex is not None and ex != '':
42 | raise RemoteException(ex)
43 | return response.result
44 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc/invoker.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | from wwwpy.common.rpc.func_registry import FuncMeta
3 |
4 |
5 | class Invoker:
6 | def __init__(self, module: FuncMeta):
7 | self.module = module
8 | self.module_type = _load_package(module.name)
9 |
10 | def __getitem__(self, item):
11 | return _Invocable(getattr(self.module_type, item))
12 |
13 |
14 | class _Invocable:
15 | def __init__(self, func):
16 | self.func = func
17 |
18 |
19 | def _load_package(package_name: str):
20 | try:
21 | return importlib.import_module(package_name)
22 | except ImportError:
23 | print(f"Package '{package_name}' could not be loaded.")
24 | return None
25 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc/serializer.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import NamedTuple, List, Any, Optional
3 |
4 |
5 | class RpcResponse(NamedTuple):
6 | result: Any
7 | exception: str
8 |
9 | @classmethod
10 | def from_json(cls, string: str) -> 'RpcResponse':
11 | obj = json.loads(string)
12 | response = RpcResponse(*obj)
13 | return response
14 |
15 | def to_json(self) -> str:
16 | return json.dumps(self, default=str)
17 |
18 |
19 | class RpcRequest(NamedTuple):
20 | module: str
21 | func: str
22 | args: List[Optional[Any]]
23 |
24 | @classmethod
25 | def to_json(cls, module_name: str, func_name: str, *args) -> str:
26 | return json.dumps(RpcRequest(module_name, func_name, args))
27 |
28 | @classmethod
29 | def from_json(cls, string: str) -> 'RpcRequest':
30 | obj = json.loads(string)
31 | request = RpcRequest(*obj)
32 | return request
33 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc/v2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/rpc/v2/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc/v2/dispatcher.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import Protocol
3 |
4 |
5 | @dataclass(frozen=True)
6 | class FunctionDef:
7 | name: str
8 | annotations: list[type]
9 | return_annotation: type
10 |
11 |
12 | @dataclass(frozen=True)
13 | class Definition:
14 | target: str
15 | functions: dict[str, FunctionDef]
16 |
17 |
18 | class Dispatcher(Protocol):
19 | """
20 | This Caller/Callee convention this is the Caller.
21 | https://en.wikipedia.org/wiki/Calling_convention
22 |
23 | This is used by the proxy_generator to be used inside the generated source code.
24 | The proxy_generator will instantiate a Dispatcher for the module and one for each class.
25 |
26 | It requires the __module__ and __qualname__ attributes. because they will be they will be imported
27 | in the generated source code.
28 |
29 | For example, classes and functions have these attributes.
30 | """
31 | __module__: str
32 | __qualname__: str
33 |
34 | def definition_complete(self, definition: Definition) -> None:
35 | """The proxy_generator will call this method when the top level module
36 | is parsed and also at the end of each class definition. This allows the implementation to
37 | inspect the functions and their type hints through the locals() dictionary."""
38 | ...
39 |
40 | def dispatch_sync(self, function_name: str, *args) -> any:
41 | """The proxy_generator will call this method to dispatch a function call to the implementation."""
42 | ...
43 |
44 | async def dispatch_async(self, function_name: str, *args) -> any:
45 | """The proxy_generator will call this method to dispatch a function call to the implementation."""
46 | ...
47 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/common/rpc2/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc2/encoder_decoder.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TypeVar, Union, Type
4 |
5 | from wwwpy.common.rpc import serialization
6 |
7 | T = TypeVar('T')
8 |
9 |
10 | class EncoderDecoderType:
11 | buffer_type: Union[bytes, str]
12 |
13 |
14 | class Encoder(EncoderDecoderType):
15 | buffer: bytes | str
16 |
17 | def encode(self, obj: any, cls: Type[T]):
18 | """Add the object to the encoder"""
19 |
20 |
21 | class Decoder(EncoderDecoderType):
22 | def decode(self, cls: Type[T]) -> T:
23 | """Get the next object of the given type"""
24 |
25 |
26 | class EncoderDecoder:
27 | def decoder(self, buffer: str | bytes) -> Decoder: raise NotImplementedError
28 |
29 | def encoder(self) -> Encoder: raise NotImplementedError
30 |
31 |
32 | class JsonEncoder(Encoder):
33 | def __init__(self, sep: str):
34 | self._sep = sep
35 | self._buffer = []
36 |
37 | def encode(self, obj: any, cls: Type[T]):
38 | self._buffer.append(serialization.to_json(obj, cls))
39 |
40 | @property
41 | def buffer(self) -> str:
42 | return self._sep.join(self._buffer)
43 |
44 |
45 | class JsonDecoder(Decoder):
46 | def __init__(self, buffer: str, sep: str):
47 | self._buffer = iter(buffer.split(sep))
48 |
49 | def decode(self, cls: Type[T]) -> T:
50 | item = next(self._buffer)
51 | return serialization.from_json(item, cls)
52 |
53 |
54 | class JsonEncoderDecoder(EncoderDecoder):
55 |
56 | def __init__(self):
57 | self._sep = '\n'
58 |
59 | def decoder(self, buffer: str | bytes) -> Decoder:
60 | return JsonDecoder(buffer, sep=self._sep)
61 |
62 | def encoder(self) -> Encoder:
63 | return JsonEncoder(sep=self._sep)
64 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc2/skeleton.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 |
4 | class Skeleton:
5 | def invoke_sync(self): raise NotImplementedError
6 |
7 | async def invoke_async(self): raise NotImplementedError
8 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc2/transport.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 |
4 | class Transport:
5 |
6 | def send_sync(self, payload: str | bytes): raise NotImplementedError
7 |
8 | async def send_async(self, payload: str | bytes): raise NotImplementedError
9 |
10 | def recv_sync(self) -> str | bytes: raise NotImplementedError
11 |
12 | async def recv_async(self) -> str | bytes: raise NotImplementedError
13 |
--------------------------------------------------------------------------------
/src/wwwpy/common/rpc2/typed_function.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import inspect
4 | import types
5 | import typing
6 | from dataclasses import dataclass
7 |
8 |
9 | @dataclass
10 | class TypedFunction:
11 | module_name: str
12 | func_name: str
13 | args_types: list[type]
14 | return_type: type # this MUST be present also for void functions
15 | is_coroutine: bool
16 |
17 | def __post_init__(self):
18 | if self.return_type is None:
19 | raise Exception(f'Return type missing for function {self.func_name}')
20 |
21 |
22 | def get_typed_function(function: types.FunctionType) -> TypedFunction:
23 | type_hints = typing.get_type_hints(function) # to be used if the below code do not resolve types
24 |
25 | signature = inspect.signature(function)
26 | args_types = []
27 | for name, param in signature.parameters.items():
28 | annotation = type_hints.get(name, None)
29 | if annotation is None:
30 | raise Exception(
31 | f'There is no support for not annotated arguments. Annotation missing for parameter {name} in function {function.__name__}')
32 | args_types.append(annotation)
33 |
34 | return_annotation = type_hints.get('return', type(None))
35 | return TypedFunction(
36 | function.__module__,
37 | function.__name__,
38 | args_types,
39 | return_annotation,
40 | inspect.iscoroutinefunction(function)
41 | )
42 |
--------------------------------------------------------------------------------
/src/wwwpy/common/settingslib.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import logging
3 | import configparser
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 |
8 | class Settings:
9 | def __init__(self):
10 | self._config = configparser.ConfigParser()
11 |
12 | def load(self, ini_file: Path):
13 | logger.debug(f'Loading settings from {ini_file}')
14 | self._config.read(ini_file)
15 |
16 | @property
17 | def hotreload_self(self) -> bool:
18 | return self._config.getboolean('general', 'hotreload_self', fallback=False)
19 |
20 | @property
21 | def open_url_code(self) -> str:
22 | return self._config.get('general', 'open_url_code', fallback='')
23 |
24 | @property
25 | def log_level(self) -> dict[str, str]:
26 | if not self._config.has_section('log_level'):
27 | return {}
28 | return dict(self._config.items('log_level'))
29 |
--------------------------------------------------------------------------------
/src/wwwpy/common/strings.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 |
4 | def id_to_hex(obj) -> str:
5 | """Convert an object's ID to a shorter hexadecimal representation."""
6 | return hex(id(obj)).upper()[2:]
7 |
--------------------------------------------------------------------------------
/src/wwwpy/common/time_logger.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | import time
5 | from datetime import timedelta
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | class TimeLogger:
11 | def __init__(self, name):
12 | self.name = name
13 | self.start = time.perf_counter()
14 |
15 | def debug(self, message=''):
16 | logger.debug(self.message(message), stacklevel=2)
17 |
18 | def message(self, msg='') -> str:
19 | delta = self.time_spent()
20 | if msg:
21 | msg = f' - {msg}'
22 | return f'{self.name}{msg} time={delta}'
23 |
24 | def time_spent(self) -> timedelta:
25 | end = time.perf_counter()
26 | return timedelta(seconds=end - self.start)
27 |
--------------------------------------------------------------------------------
/src/wwwpy/common/tree.py:
--------------------------------------------------------------------------------
1 | # tree.py
2 | # credits to: https://stackoverflow.com/a/59109706/316766
3 |
4 | from pathlib import Path
5 | from typing import Protocol, Iterable
6 |
7 | # prefix components:
8 | space = ' '
9 | branch = '│ '
10 | # pointers:
11 | tee = '├── '
12 | last = '└── '
13 |
14 |
15 | class NodeProtocol(Protocol):
16 | def iterdir(self) -> Iterable['NodeProtocol']: ...
17 |
18 | def is_dir(self) -> bool: ...
19 |
20 | @property
21 | def name(self) -> str: ...
22 |
23 |
24 | def tree(dir_path: NodeProtocol, prefix: str = '', file_size=False):
25 | """A recursive generator, given a directory Path object
26 | will yield a visual tree structure line by line
27 | with each line prefixed by the same characters
28 | """
29 | contents = list(dir_path.iterdir())
30 | # contents each get pointers that are ├── with a final └── :
31 | pointers = [tee] * (len(contents) - 1) + [last]
32 | for pointer, path in zip(pointers, contents):
33 | fs = '' if not file_size or path.is_dir() else f' ({path.stat().st_size})'
34 | yield prefix + pointer + path.name + fs
35 | try:
36 | if path.is_dir(): # extend the prefix and recurse:
37 | extension = branch if pointer == tee else space
38 | # i.e. space because last, └── , above so no more |
39 | yield from tree(path, prefix=prefix + extension, file_size=file_size)
40 | except Exception as e:
41 | yield prefix + f'Error: {e}'
42 |
43 |
44 | def print_tree(path, printer=print, file_size=True):
45 | for line in tree(Path(path), file_size=file_size):
46 | printer(line)
47 |
48 |
49 | def filesystem_tree_str(path: Path | str) -> str:
50 | path = Path(path)
51 | if path.exists():
52 | tree_str = '\n'.join(tree(path, prefix=' ', file_size=True))
53 | return f'Filesystem tree for {path}:\n{tree_str}'
54 | else:
55 | return f'Filesystem path do not exists `{path}`'
56 |
57 |
58 | if __name__ == '__main__':
59 | print_tree(Path(__file__).parent.parent, file_size=True)
60 |
--------------------------------------------------------------------------------
/src/wwwpy/common/type_listener.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TypeVar, Generic, Callable
4 |
5 | T = TypeVar('T')
6 |
7 |
8 | class TypeListeners(Generic[T], list[Callable[[T], None]]):
9 | def __init__(self, event_type: type[T] | None) -> None:
10 | super().__init__()
11 | self.event_type = event_type
12 |
13 | def add(self, handler: Callable[[T], None]) -> None:
14 | self.append(handler)
15 |
16 | def remove(self, handler: Callable[[T], None]) -> None:
17 | super().remove(handler)
18 |
19 | def notify(self, event: T) -> None:
20 | if self.event_type and not isinstance(event, self.event_type):
21 | raise TypeError(f'Handler expects {self.event_type}')
22 | for h in list(self):
23 | h(event)
24 |
25 |
26 | class DictListeners:
27 | def __init__(self):
28 | self._listeners: dict[type, TypeListeners] = {}
29 | self.catch_all = TypeListeners(None)
30 |
31 | def on(self, event_type: type[T]) -> TypeListeners[T]:
32 | lst = self._listeners.get(event_type)
33 | if lst is None:
34 | lst = TypeListeners(event_type)
35 | self._listeners[event_type] = lst
36 | return lst
37 |
38 | def notify(self, ev: T) -> None:
39 | listeners = self._listeners.get(type(ev), None)
40 | if listeners:
41 | listeners.notify(ev)
42 | self.catch_all.notify(ev)
43 |
--------------------------------------------------------------------------------
/src/wwwpy/exceptions.py:
--------------------------------------------------------------------------------
1 | class WwwpyException(Exception):
2 | pass
3 |
4 |
5 | class RemoteException(WwwpyException):
6 | """When the server code fails"""
7 | pass
8 |
9 |
10 | class RemoteError(WwwpyException):
11 | """When network/wwwpy infrastructure fails"""
12 | pass
13 |
--------------------------------------------------------------------------------
/src/wwwpy/http.py:
--------------------------------------------------------------------------------
1 | from typing import NamedTuple, Callable, Union
2 | # todo rename this in httplib (otherwise it crash jetbrains debug mode)
3 | from wwwpy.common.asynclib import OptionalCoroutine
4 |
5 |
6 | class HttpRequest(NamedTuple):
7 | method: str
8 | content: Union[str, bytes]
9 | content_type: str
10 |
11 |
12 | class HttpResponse(NamedTuple):
13 | content: Union[str, bytes]
14 | content_type: str
15 |
16 | @staticmethod
17 | def application_zip(content: bytes) -> 'HttpResponse':
18 | content_type = 'application/zip, application/octet-stream, application/x-zip-compressed, multipart/x-zip'
19 | return HttpResponse(content, content_type)
20 |
21 | @staticmethod
22 | def text_html(content: str) -> 'HttpResponse':
23 | return HttpResponse(content, 'text/html')
24 |
25 |
26 | class HttpRoute(NamedTuple):
27 | path: str
28 | callback: Callable[[HttpRequest, Callable[[HttpResponse], OptionalCoroutine]], OptionalCoroutine]
29 |
--------------------------------------------------------------------------------
/src/wwwpy/platformdirs/__main__.py:
--------------------------------------------------------------------------------
1 | """Main entry point."""
2 |
3 | from __future__ import annotations
4 |
5 | from . import PlatformDirs, __version__
6 |
7 | PROPS = (
8 | "user_data_dir",
9 | "user_config_dir",
10 | "user_cache_dir",
11 | "user_state_dir",
12 | "user_log_dir",
13 | "user_documents_dir",
14 | "user_downloads_dir",
15 | "user_pictures_dir",
16 | "user_videos_dir",
17 | "user_music_dir",
18 | "user_runtime_dir",
19 | "site_data_dir",
20 | "site_config_dir",
21 | "site_cache_dir",
22 | "site_runtime_dir",
23 | )
24 |
25 |
26 | def main() -> None:
27 | """Run the main entry point."""
28 | app_name = "MyApp"
29 | app_author = "MyCompany"
30 |
31 | print(f"-- platformdirs {__version__} --") # noqa: T201
32 |
33 | print("-- app dirs (with optional 'version')") # noqa: T201
34 | dirs = PlatformDirs(app_name, app_author, version="1.0")
35 | for prop in PROPS:
36 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
37 |
38 | print("\n-- app dirs (without optional 'version')") # noqa: T201
39 | dirs = PlatformDirs(app_name, app_author)
40 | for prop in PROPS:
41 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
42 |
43 | print("\n-- app dirs (without optional 'appauthor')") # noqa: T201
44 | dirs = PlatformDirs(app_name)
45 | for prop in PROPS:
46 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
47 |
48 | print("\n-- app dirs (with disabled 'appauthor')") # noqa: T201
49 | dirs = PlatformDirs(app_name, appauthor=False)
50 | for prop in PROPS:
51 | print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
52 |
53 |
54 | if __name__ == "__main__":
55 | main()
56 |
--------------------------------------------------------------------------------
/src/wwwpy/platformdirs/readme.txt:
--------------------------------------------------------------------------------
1 | licence in https://github.com/wwwpy-labs/wwwpy/tree/main/licenses
2 |
3 | originally from (see below)
4 |
5 | git clone https://github.com/tox-dev/platformdirs
6 | git checkout ee86084986d64209f228c9059eabdf791865f758
7 |
--------------------------------------------------------------------------------
/src/wwwpy/platformdirs/version.py:
--------------------------------------------------------------------------------
1 | # file generated by setuptools_scm
2 | # don't change, don't track in version control
3 | TYPE_CHECKING = False
4 | if TYPE_CHECKING:
5 | from typing import Tuple, Union
6 | VERSION_TUPLE = Tuple[Union[int, str], ...]
7 | else:
8 | VERSION_TUPLE = object
9 |
10 | version: str
11 | __version__: str
12 | __version_tuple__: VERSION_TUPLE
13 | version_tuple: VERSION_TUPLE
14 |
15 | __version__ = version = '4.3.7.dev13+g7ed2ea6'
16 | __version_tuple__ = version_tuple = (4, 3, 7, 'dev13', 'g7ed2ea6')
17 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 |
8 | # PUBLIC-API
9 | def dict_to_js(o):
10 | import js
11 | import pyodide
12 | return pyodide.ffi.to_js(o, dict_converter=js.Object.fromEntries)
13 |
14 |
15 | async def micropip_install(package):
16 | from js import pyodide
17 | await pyodide.loadPackage('micropip')
18 | import micropip
19 | await micropip.install([package])
20 |
21 |
22 | # def set_timeout(callback: Callable[[], Union[None, Awaitable[None]]], timeout_millis: int | None = 0):
23 | # from pyodide.ffi import create_once_callable
24 | # from js import window
25 | # window.setTimeout(create_once_callable(callback), timeout_millis)
26 |
27 |
28 | def dict_to_py(js_obj):
29 | import js
30 | py_dict = {}
31 | # Iterate through properties, including those in the prototype chain
32 | current = js_obj
33 | while current:
34 | for prop in js.Object.getOwnPropertyNames(current):
35 | try:
36 | value = getattr(js_obj, prop)
37 | if not callable(value): # Skip methods
38 | _ = str(value) # some properties throw errors when accessed
39 | py_dict[prop] = value
40 | except Exception as ex:
41 | try:
42 | logger.warning(f'Error accessing property `{prop}` of {current}: {ex}')
43 | except:
44 | pass
45 | current = js.Object.getPrototypeOf(current)
46 |
47 | return py_dict
48 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/databind/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/remote/databind/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/remote/databind/bind_wrapper.py:
--------------------------------------------------------------------------------
1 | from wwwpy.common.databind.databind import TargetAdapter
2 | import js
3 |
4 | from pyodide.ffi import create_proxy
5 |
6 | from wwwpy.common.property_monitor import PropertyChanged
7 |
8 | # rename to HTMLInputTargetAdapter or HTMLInputTarget
9 | class InputTargetAdapter(TargetAdapter):
10 | def __init__(self, inp: js.HTMLInputElement):
11 | super().__init__()
12 | self.input = inp
13 | self.input.addEventListener('input', create_proxy(self._new_input_event))
14 |
15 | def set_target_value(self, value):
16 | self.input.value = value
17 |
18 | def get_target_value(self):
19 | return self.input.value
20 |
21 | def _new_input_event(self, event):
22 | self.monitor_object.notify([PropertyChanged(self, '', None, self.input.value)])
23 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/remote/designer/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/dev_mode_events.py:
--------------------------------------------------------------------------------
1 | class AfterDevModeShow:
2 | """This event is published after the dev mode component is added to the DOM"""
3 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/di_remote.py:
--------------------------------------------------------------------------------
1 | from wwwpy.remote.designer.ui import palette, pushable_sidebar, floater_action_band
2 |
3 |
4 | def register_bindings():
5 | palette.extension_point_register()
6 | pushable_sidebar.register_extension_point()
7 | floater_action_band.extension_point_register()
8 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/global_interceptor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass
4 | from typing import Callable
5 |
6 | import js
7 | from js import Event, document, HTMLElement
8 | from pyodide.ffi import create_proxy
9 |
10 |
11 | @dataclass
12 | class InterceptorEvent:
13 | target: HTMLElement | None
14 | event: Event
15 | interceptor: GlobalInterceptor
16 |
17 | def uninstall(self):
18 | self.interceptor.uninstall()
19 |
20 | def preventAndStop(self):
21 | if self.event:
22 | self.event.preventDefault()
23 | self.event.stopImmediatePropagation()
24 | self.event.stopPropagation()
25 |
26 |
27 | class GlobalInterceptor:
28 |
29 | def __init__(self, callback: Callable[[InterceptorEvent], None], event_name: str = 'click'):
30 | self._callback = callback
31 | self._event_name = event_name
32 | self._installed = False
33 | self._proxy = create_proxy(self._handler)
34 |
35 | def install(self):
36 | if self._installed:
37 | return
38 | self._installed = True
39 | js.document.addEventListener(self._event_name, self._proxy, True)
40 |
41 | def uninstall(self):
42 | if not self._installed:
43 | return
44 | self._installed = False
45 | js.document.removeEventListener(self._event_name, self._proxy, True)
46 |
47 | def _handler(self, event: Event):
48 | ev = InterceptorEvent(event.target, event, self)
49 | self._callback(ev)
50 |
51 |
52 | def global_interceptor_start(callback: Callable[[InterceptorEvent], None]):
53 | proxy = []
54 |
55 | def _uninstall():
56 | document.removeEventListener('click', proxy[0], True)
57 | proxy.clear()
58 |
59 | def global_click(event: Event):
60 | ev = InterceptorEvent(event.target, event, _uninstall)
61 | callback(ev)
62 |
63 | proxy.append(create_proxy(global_click))
64 | document.addEventListener('click', proxy[0], True)
65 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/log_redirect.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from js import console
4 | from wwwpy.common.designer import log_emit
5 | from wwwpy.server.designer import rpc
6 |
7 | _log_buffer = asyncio.Queue()
8 |
9 |
10 | async def _process_buffer():
11 | """This technique avoids the issue of wrong log order"""
12 | while True:
13 | msg = await _log_buffer.get()
14 | await rpc.server_console_log(msg)
15 |
16 |
17 | def redirect_logging():
18 | asyncio.create_task(_process_buffer())
19 |
20 | def emit(msg: str):
21 | console.log(msg)
22 | # this is not correct because we are not in a coroutine
23 | # but it probably works because we are in the browser with just one event loop
24 | _log_buffer.put_nowait(msg)
25 |
26 | log_emit.add_once(emit)
27 | log_emit.warning_to_log()
28 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/rpc.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import List, Any
3 |
4 | from wwwpy.common import files
5 |
6 |
7 | class DesignerRpc:
8 |
9 | def hotreload_notify_changes(self, do_reload: bool, events: List[Any]):
10 | directory = Path(files._bundle_path)
11 |
12 | from wwwpy.common.filesystem.sync import Sync, sync_delta2
13 | sync_impl: Sync = sync_delta2
14 | sync_impl.sync_target(directory, events)
15 | if do_reload:
16 | self.hotreload_do()
17 |
18 | def hotreload_do(self):
19 | from wwwpy.remote.browser_main import _reload
20 | _reload()
21 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/__init__.py:
--------------------------------------------------------------------------------
1 | # comment
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/floater.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from wwwpy.common.designer.ui.rect_readonly import RectReadOnly
4 | from wwwpy.remote import component as wpc
5 |
6 |
7 | class Floater(wpc.Component, auto_define=False):
8 | """This is a floating component that will adapt itself to the given situation"""
9 |
10 | def set_reference_geometry(self, rect: RectReadOnly):
11 | """Set the geometry of the element that we are attached to."""
12 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/floater_selection_indicator.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import js
4 |
5 | from wwwpy.common.designer.ui.rect_readonly import RectReadOnly
6 | from wwwpy.remote import dict_to_js
7 | from wwwpy.remote.designer.ui.floater import Floater
8 |
9 |
10 | class SelectionIndicatorFloater(Floater, tag_name='selection-indicator-floater'):
11 |
12 | def init_component(self):
13 | self.element.attachShadow(dict_to_js({'mode': 'open'}))
14 | # language=html
15 | self.element.shadowRoot.innerHTML = """
16 |
26 | """
27 |
28 | @property
29 | def transition(self) -> bool:
30 | return self.element.style.transition != 'none'
31 |
32 | @transition.setter
33 | def transition(self, value: bool):
34 | if value:
35 | self.element.style.transition = 'all 0.2s ease'
36 | else:
37 | self.element.style.transition = 'none'
38 |
39 | @property
40 | def visible(self) -> bool:
41 | return self.element.style.display == 'block'
42 |
43 | def hide(self):
44 | self.element.style.display = 'none'
45 |
46 | def set_reference_geometry(self, rect: RectReadOnly):
47 | bs = 2 # Adjust this value to match the border size in CSS
48 |
49 | rect = js.DOMRect.new(rect.x - bs, rect.y - bs, rect.width, rect.height, )
50 |
51 | self.element.style.display = 'block'
52 | self.element.style.top = f"{rect.top}px"
53 | self.element.style.left = f"{rect.left}px"
54 | self.element.style.width = f"{rect.width}px"
55 | self.element.style.height = f"{rect.height}px"
56 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/help_icon.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import js
4 |
5 | import wwwpy.remote.component as wpc
6 | from wwwpy.remote import dict_to_js
7 |
8 |
9 | class HelpIcon(wpc.Component, tag_name='wwwpy-help-icon'):
10 | _link: js.HTMLElement = wpc.element()
11 | href: str = wpc.attribute()
12 |
13 | def init_component(self):
14 | self.element.attachShadow(dict_to_js({'mode': 'open'}))
15 | # language=html
16 | self.element.shadowRoot.innerHTML = """
17 |
25 |
31 |
32 |
33 | """
34 | self._update_href()
35 |
36 | def _link__click(self, event: js.MouseEvent):
37 | event.stopPropagation()
38 | if not self.href:
39 | event.preventDefault()
40 |
41 | def attributeChangedCallback(self, name: str, oldValue: str, newValue: str):
42 | if name == 'href':
43 | self._update_href()
44 |
45 | def _update_href(self):
46 | self._link.href = self.href
47 |
48 | @property
49 | def visible(self):
50 | return self.element.style.visibility != 'hidden'
51 |
52 | @visible.setter
53 | def visible(self, value):
54 | self.element.style.visibility = 'visible' if value else 'hidden'
55 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/intent_select_element.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from dataclasses import dataclass
3 | from functools import cached_property
4 |
5 | import js
6 |
7 | from wwwpy.common.designer.canvas_selection import CanvasSelection
8 | from wwwpy.common.injectorlib import injector
9 | from wwwpy.remote.designer.ui.element_selector import ElementSelector
10 | from wwwpy.remote.designer.ui.intent import Intent
11 | from wwwpy.remote.designer.ui.locator_event import LocatorEvent
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | @dataclass
17 | class SelectElementIntent(Intent):
18 | """Action to select an element in the designer."""
19 | label: str = 'Select'
20 | icon: str = 'select_element_icon'
21 |
22 | def on_hover(self, event: LocatorEvent):
23 | logger.debug(f'on_hover {event}')
24 | self._set_selection_from_js_event(event)
25 |
26 | def on_submit(self, event: LocatorEvent) -> bool:
27 | logger.debug(f'on_submit {event}')
28 | self._set_selection_from_js_event(event)
29 | injector.get(CanvasSelection).current_selection = event.locator
30 | return True
31 |
32 | def _set_selection_from_js_event(self, le: LocatorEvent):
33 | element_selector: ElementSelector = self._element_selector
34 | if not element_selector.element.isConnected:
35 | js.document.body.appendChild(element_selector.element)
36 |
37 | target = None
38 | if le is not None:
39 | target = le.main_element
40 |
41 | if element_selector.get_selected_element() != target:
42 | element_selector.set_selected_element(target)
43 |
44 | return target
45 |
46 | @cached_property
47 | def _element_selector(self) -> ElementSelector:
48 | return ElementSelector()
49 |
50 |
51 | def _pretty(node: js.HTMLElement):
52 | if hasattr(node, 'tagName'):
53 | identifier = node.dataset.name if node.hasAttribute('data-name') else node.id
54 | return f'{node.tagName.lower()}#{identifier}.{node.className}[{node.innerHTML.strip()[:20]}…]'
55 | return str(node)
56 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/mailto_component.py:
--------------------------------------------------------------------------------
1 | import wwwpy.remote.component as wpc
2 | import js
3 |
4 | import logging
5 | import urllib.parse
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | class MailtoComponent(wpc.Component):
11 | recipient: str = wpc.attribute()
12 | subject: str = wpc.attribute()
13 | body: str = wpc.attribute()
14 | text_content: str = wpc.attribute()
15 | target: js.HTMLElement = wpc.attribute()
16 |
17 | _link: js.HTMLElement = wpc.element()
18 |
19 | def init_component(self):
20 | # language=html
21 | self.element.innerHTML = """"""
22 |
23 | self._update_attributes()
24 |
25 | def attributeChangedCallback(self, name: str, oldValue: str, newValue: str):
26 | self._update_attributes()
27 |
28 | def connectedCallback(self):
29 | self._update_attributes()
30 |
31 | def _update_attributes(self):
32 | target = self.target if self.target else '_blank'
33 | recipient = self.recipient if self.recipient else 'foo@example.com'
34 | subject = self.subject if self.subject else 'subject not specified'
35 | body = self.body if self.body else ''
36 | text_content = self.text_content if self.text_content else f"Send Email to {recipient}"
37 |
38 | # make sure to encode the subject and body
39 | subject = urllib.parse.quote_plus(subject)
40 | body = urllib.parse.quote_plus(body)
41 |
42 | self._link.href = f"mailto:{recipient}?subject={subject}&body={body}"
43 | self._link.textContent = text_content
44 | self._link.target = target
45 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/python_console.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from dataclasses import dataclass
3 |
4 | import wwwpy.remote.component as wpc
5 | import js # used in globals
6 | from js import pyodide, document, console, window
7 |
8 | import logging
9 |
10 | from wwwpy.common import state
11 |
12 | logger = logging.getLogger(__name__)
13 | logger.setLevel(logging.DEBUG)
14 |
15 |
16 | @dataclass
17 | class State:
18 | python_code: str = 'logger.info("Hello")'
19 |
20 |
21 | class PythonConsoleComponent(wpc.Component, tag_name='wwwpy-python-console'):
22 | _ta_python_code: js.HTMLTextAreaElement = wpc.element()
23 |
24 | def init_component(self):
25 | # language=html
26 | self.element.innerHTML = """
27 | Press CTRL-Enter to run Python code
28 |
30 | """
31 | self._state = state._restore(State)
32 | self._ta_python_code.value = self._state.python_code
33 |
34 | async def _ta_python_code__keydown(self, event):
35 | if event.ctrlKey and event.key == 'Enter':
36 | await js.pyodide.runPythonAsync(self._ta_python_code.value, globals=globals())
37 |
38 | async def _ta_python_code__input(self, event):
39 | self._state.python_code = self._ta_python_code.value
40 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/quickstart_ui.py:
--------------------------------------------------------------------------------
1 | import js
2 | from pyodide.ffi import create_proxy
3 | import asyncio
4 | from wwwpy.common import quickstart
5 | from wwwpy.remote.designer.ui.window_component import new_window, WindowComponent
6 | from wwwpy.remote.designer.ui.searchable_list_1 import SearchableList1, Item
7 | from wwwpy.server.designer import rpc
8 |
9 |
10 | class QuickstartUI:
11 |
12 | def __init__(self):
13 | self.window: WindowComponent = new_window("Select a quickstart", closable=False)
14 | # self.window.set_size('300px', '300px')
15 | self.window.set_position('5px', '5px')
16 | cmp1 = SearchableList1()
17 | cmp1.root_element().style.color = 'white'
18 | quickstart_list = quickstart.quickstart_list()
19 | self.quickstart_list = quickstart_list
20 | cmp1.items = [Item(qs.title, qs.description, {'quickstart': qs}) for qs in quickstart_list]
21 | cmp1.placeholder = 'Search or select below...'
22 | self.window.element.append(cmp1.element)
23 | self.on_done = lambda *args: None
24 |
25 | cmp1.element.addEventListener('item-click', create_proxy(self._item_click_handler))
26 |
27 | def accept_quickstart(self, quickstart_name: str):
28 | self.window.element.remove()
29 |
30 | async def _notify_server():
31 | await rpc.quickstart_apply(quickstart_name)
32 | self.on_done(quickstart_name)
33 |
34 | asyncio.create_task(_notify_server())
35 |
36 | def _item_click_handler(self, event):
37 | item = event.detail
38 | assert isinstance(item, Item)
39 | self.accept_quickstart(item.values['quickstart'].name)
40 |
41 |
42 | def create() -> QuickstartUI:
43 | return QuickstartUI()
44 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/system_tools/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/remote/designer/ui/system_tools/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/remote/designer/ui/system_tools/system_versions.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import js
4 |
5 | import wwwpy.remote.component as wpc
6 | from wwwpy.remote.jslib import waitAnimationFrame
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class SystemVersions(wpc.Component, tag_name='wwwpy-system-versions'):
12 | _container: js.HTMLElement = wpc.element()
13 | _wwwpy: js.HTMLElement = wpc.element()
14 | _python_remote: js.HTMLElement = wpc.element()
15 | _python_server: js.HTMLElement = wpc.element()
16 | _pyodide: js.HTMLElement = wpc.element()
17 |
18 | def init_component(self):
19 | # language=html
20 | self.element.innerHTML = """
21 |
22 | wwwpy==...
23 | pyodide==...
24 |
25 | Python versions:
26 | pyton remote==...
27 | pyton server==...
28 |
"""
29 |
30 | async def after_init_component(self):
31 | await waitAnimationFrame()
32 |
33 | import platform
34 | pyver = platform.python_version()
35 |
36 | try:
37 | import wwwpy
38 | wp_ver = wwwpy.__version__
39 | except Exception as e:
40 | wp_ver = e.message
41 |
42 | try:
43 | import js
44 | pyodide_ver = js.window.pyodide.version
45 | except Exception as e:
46 | pyodide_ver = e.message
47 |
48 | self._wwwpy.innerText = f'wwwpy==' + wp_ver
49 | self._pyodide.innerText = 'pyodide==' + pyodide_ver
50 | self._python_remote.innerText = 'remote==' + pyver + ' (this is tied to Pyodide version)'
51 |
52 | from wwwpy.server.designer import rpc
53 | server_version = await rpc.server_python_version_string()
54 | self._python_server.innerText = 'server==' + server_version
55 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/fetch.py:
--------------------------------------------------------------------------------
1 | from js import fetch, console
2 |
3 | from wwwpy.common.fetch_debug import fetch_debug
4 |
5 | # logger = logging.getLogger(__name__)
6 | logger = console # if log redirect is active, it will create an infinite loop because
7 |
8 |
9 | async def async_fetch_str(url: str, method: str = 'GET', data: str = '') -> str:
10 | logger.debug(__name__ + ' ' + fetch_debug(url, method, data))
11 | response = await fetch(url, method=method, body=data)
12 | text = await response.text()
13 | return text
14 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/files.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import mimetypes
3 |
4 | from js import console, document
5 | from pathlib import Path
6 |
7 |
8 | def download_path(filename: str, path: Path, mime_type: str = None):
9 | download_bytes(filename, path.read_bytes(), mime_type)
10 |
11 |
12 | def download_bytes(filename: str, content: bytes, mime_type: str = None):
13 | if mime_type is None:
14 | gt = mimetypes.guess_type(filename)
15 | mime_type = gt[0]
16 | if mime_type is None:
17 | mime_type = 'application/octet-stream'
18 | console.log(f'guess mime type for `{filename}` is `{mime_type}`')
19 | a = document.createElement('a')
20 | a.download = filename
21 | a.href = f'data:{mime_type};base64,{base64.b64encode(content).decode("ascii")}'
22 | document.body.append(a)
23 | a.click()
24 |
25 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/idbfs.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from pathlib import Path
4 |
5 | import js
6 | from pyodide.ffi import create_proxy, to_js
7 | from wwwpy.common.asynclib import create_task_safe
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | async def _fs_idbfs_sync(n):
13 | queue = asyncio.Queue(1)
14 | js.pyodide.FS.syncfs(n, create_proxy(lambda err: queue.put_nowait(err)))
15 | res = await asyncio.wait_for(queue.get(), 5)
16 | logger.debug(f'fs_idbfs_sync({n}) res: {res}')
17 | return res
18 |
19 |
20 | def fs_idbfs_load(): return create_task_safe(_fs_idbfs_sync(1))
21 |
22 |
23 | def fs_idbfs_save(): return create_task_safe(_fs_idbfs_sync(0))
24 |
25 |
26 | async def fs_idbfs_mount(mount_point: str):
27 | if Path(mount_point).exists():
28 | return logger.warning('already mounted')
29 | js.pyodide.FS.mkdirTree(mount_point)
30 | js.pyodide.FS.mount(js.pyodide.FS.filesystems.IDBFS, to_js({}), mount_point)
31 | await fs_idbfs_load()
32 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/remote_fixtures.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import js
4 | import pytest
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | @pytest.fixture
10 | def clean_document():
11 | clean_document_now('begin')
12 | yield None
13 | clean_document_now('end')
14 |
15 |
16 | def clean_document_now(mode='mode/NA'):
17 | # logger.debug(f'_clean_document {mode}')
18 | js.document.documentElement.innerHTML = ''
19 | js.document.head.innerHTML = ''
20 | for attr in js.document.documentElement.attributes:
21 | js.document.documentElement.removeAttributeNode(attr)
22 |
23 | # logger.debug(f'body=```{js.document.body.innerHTML}```')
24 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/root_path.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass
4 | from pathlib import Path
5 |
6 | from js import console
7 |
8 | from wwwpy.common import modlib
9 |
10 |
11 | @dataclass
12 | class _Dir:
13 | root: Path
14 | remote: Path
15 |
16 |
17 | def _get_dir():
18 | # this works because both wwwpy and user code are flattened in the same folder
19 | root = modlib._find_module_path('wwwpy').parent.parent
20 | console.log(f'root={root}')
21 | remote = root / 'remote'
22 | return _Dir(root, remote)
23 |
--------------------------------------------------------------------------------
/src/wwwpy/remote/shoelace.py:
--------------------------------------------------------------------------------
1 | from js import document
2 | import js
3 |
4 | from wwwpy.common.asynclib import create_task_safe
5 | from wwwpy.remote.jslib import script_load_once
6 |
7 | _version = '2.20.0'
8 | _css_url = f'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{_version}/cdn/themes/dark.css'
9 | _js_url = f'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{_version}/cdn/shoelace.js'
10 |
11 | _task = []
12 |
13 |
14 | def setup_shoelace():
15 | async def _setup_shoelace():
16 | needed = await script_load_once(_js_url, type='module')
17 | if needed:
18 | document.documentElement.className = 'sl-theme-dark'
19 | document.head.append(document.createRange().createContextualFragment(_head_style))
20 |
21 | return create_task_safe(_setup_shoelace())
22 |
23 |
24 | #
25 |
26 | # language=html
27 | _head_style = f"""
28 |
29 |
30 |
38 | """
39 | _head_script = f""""""
40 |
41 |
42 | # alternative
43 | # language=html
44 | _head_style_dark_palin ="""
45 |
46 |
58 | """
--------------------------------------------------------------------------------
/src/wwwpy/remote/websocket.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Callable
4 |
5 | from wwwpy.common.rpc.serializer import RpcRequest
6 | import asyncio
7 |
8 | import logging
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | async def setup_websocket():
14 | from js import window, console
15 | import importlib
16 | def log(msg):
17 | console.log(msg)
18 |
19 | def message(msg):
20 | log(f'message:{msg}')
21 | r = RpcRequest.from_json(msg)
22 | # _debug_requested_module(r.module)
23 | m = importlib.import_module(r.module)
24 | class_name, func_name = r.func.split('.')
25 | attr = getattr(m, class_name)
26 | inst = attr()
27 | func = getattr(inst, func_name)
28 | func(*r.args)
29 |
30 | l = window.location
31 | proto = 'ws' if l.protocol == 'http:' else 'wss'
32 | url = f'{proto}://{l.host}/wwwpy/ws'
33 | _WebSocketReconnect(url, message)
34 |
35 |
36 | class _WebSocketReconnect:
37 | def __init__(self, url: str, on_message: Callable):
38 | self._url = url
39 | self._on_message = on_message
40 | self._counter = 0
41 | self._connect()
42 |
43 | def _connect(self):
44 | from js import WebSocket, console
45 | self._counter += 1
46 | console.log(f'connecting to {self._url} counter={self._counter}')
47 | es = WebSocket.new(self._url)
48 | es.onopen = lambda e: console.log('open')
49 | es.onmessage = lambda e: self._on_message(e.data)
50 | es.onerror = lambda e: es.close()
51 |
52 | async def reconnect():
53 | await asyncio.sleep(1)
54 | self._connect()
55 |
56 | es.onclose = lambda e: asyncio.create_task(reconnect()) # for now, we just retry forever and ever
57 |
58 |
59 | def _debug_requested_module(module_name: str):
60 | from wwwpy.common import modlib
61 | mp = modlib._find_module_path(module_name)
62 | logger.debug(f'requested module: {module_name} path: {mp}')
63 | if mp:
64 | logger.debug(f'content: ```{mp.read_text()}```')
65 |
--------------------------------------------------------------------------------
/src/wwwpy/server/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/server/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/server/__main__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import argparse
4 | import logging
5 | import os
6 | from pathlib import Path
7 | from typing import Optional, Sequence, NamedTuple
8 |
9 | from wwwpy.server.convention import start_default
10 | from wwwpy.server.tcp_port import find_port
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class Arguments(NamedTuple):
16 | directory: Path
17 | port: int
18 | dev: bool
19 |
20 |
21 | def parse_arguments(args: Optional[Sequence[str]] = None) -> Arguments:
22 | parser = argparse.ArgumentParser(prog='wwwpy')
23 | parser.add_argument('dev', nargs='?', const=True, default=False,
24 | help="Run in development mode")
25 | parser.add_argument('--directory', '-d', default=os.getcwd(),
26 | help='set the root path for the project (default: current directory)')
27 | parser.add_argument('--port', type=int, default=8000,
28 | help='bind to this port (default: 8000)')
29 |
30 | parsed_args = parser.parse_args(args)
31 | return Arguments(
32 | directory=Path(parsed_args.directory).absolute(),
33 | port=parsed_args.port,
34 | dev=bool(parsed_args.dev)
35 | )
36 |
37 |
38 | def _open_browser(args, settings):
39 | import webbrowser
40 | if settings.open_url_code:
41 | loc = {'url': f'http://localhost:{args.port}', args: args}
42 | exec(settings.open_url_code, loc, loc)
43 | else:
44 | webbrowser.open(f'http://localhost:{args.port}')
45 |
46 |
47 | def main():
48 | import wwwpy
49 | print(f'Starting wwwpy v{wwwpy.__version__}')
50 | args = parse_arguments()
51 | if args.port == 0:
52 | args = args._replace(port=find_port())
53 | project = start_default(args.directory, args.port, dev_mode=args.dev)
54 | _open_browser(args, project.settings)
55 | try:
56 | from wwwpy.webserver import wait_forever
57 | wait_forever()
58 | except KeyboardInterrupt:
59 | pass
60 |
61 |
62 | if __name__ == '__main__':
63 | main()
64 |
--------------------------------------------------------------------------------
/src/wwwpy/server/custom_str.py:
--------------------------------------------------------------------------------
1 | class CustomStr(str):
2 | pass
3 |
4 |
5 | def get_root_folder_or_fail() -> CustomStr:
6 | import sys
7 | only_custom = [p for p in sys.path if isinstance(p, CustomStr)]
8 |
9 | if len(only_custom) != 1:
10 | raise ValueError('Cannot find root folder')
11 |
12 | return only_custom[0]
13 |
--------------------------------------------------------------------------------
/src/wwwpy/server/designer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/server/designer/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/server/fetch.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | import urllib
5 | import urllib.request
6 | from typing import Any
7 |
8 | from wwwpy.http import HttpResponse
9 |
10 |
11 | async def async_fetch_str(url: str, method: str = 'GET', data: str = '') -> str:
12 | response = sync_fetch_response(url, method=method, data=data)
13 | assert isinstance(response.content, str)
14 | return response.content
15 |
16 |
17 | def sync_fetch_response(url: str, method: str = 'GET', data: str | bytes = '') -> HttpResponse:
18 | def make_response(r: Any) -> HttpResponse:
19 | return HttpResponse(
20 | r.read().decode("utf-8"),
21 | r.headers.get_content_type()
22 | )
23 |
24 | if method != 'GET':
25 | if isinstance(data, str):
26 | data_bytes = bytes(data, 'utf8')
27 | else:
28 | data_bytes = data
29 |
30 | rq = urllib.request.Request(url, method=method, data=data_bytes)
31 | with urllib.request.urlopen(rq) as r:
32 | return make_response(r)
33 | else:
34 | with urllib.request.urlopen(url) as r:
35 | return make_response(r)
36 |
37 |
38 | async def async_fetch_response(url: str, method: str = 'GET', data: str | bytes = '') -> HttpResponse:
39 | def make_response(r: Any) -> HttpResponse:
40 | return HttpResponse(
41 | r.read().decode("utf-8"),
42 | r.headers.get_content_type()
43 | )
44 |
45 | def fetch() -> HttpResponse:
46 | if method != 'GET':
47 | if isinstance(data, str):
48 | data_bytes = bytes(data, 'utf8')
49 | else:
50 | data_bytes = data
51 |
52 | rq = urllib.request.Request(url, method=method, data=data_bytes)
53 | with urllib.request.urlopen(rq) as r:
54 | return make_response(r)
55 | else:
56 | with urllib.request.urlopen(url) as r:
57 | return make_response(r)
58 |
59 | loop = asyncio.get_running_loop()
60 | return await loop.run_in_executor(None, fetch)
61 |
--------------------------------------------------------------------------------
/src/wwwpy/server/filesystem_sync/README.md:
--------------------------------------------------------------------------------
1 | # Filesystem-sync
2 |
3 | This package serves to monitor a filesystem and propagate changes to another filesystem to keep them in sync.
4 |
5 | The source and target filesystems are assumed to be on different machines and there is a small part that serializes the filesystem changes to be sent over the transport (e.g., network)
6 |
7 | The source filesystem is monitored using [watchdog](https://pypi.org/project/watchdog/).
8 |
9 | There are in essence two parts:
10 | - one part to debounce the filesystem events to avoid frenzied activity
11 | - one part that takes in the debounced events and propagates them to the target filesystem
12 |
13 | Both parts have tests and are tested in isolation.
14 |
15 | ## Debounce
16 | This is a general implementation that keeps a buffer and notifies the observer when a timeout is reached.
17 |
18 | ## Sync
19 | Take a look to the [Sync](src/filesystem_sync/sync.py) protocol
--------------------------------------------------------------------------------
/src/wwwpy/server/filesystem_sync/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/wwwpy/server/filesystem_sync/sync_zip.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import base64
4 | import os
5 | import zipfile
6 | from io import BytesIO
7 | from pathlib import Path
8 |
9 | import shutil
10 | from pathlib import Path
11 | from typing import List, Any
12 |
13 | from wwwpy.common.filesystem.sync import Event
14 |
15 |
16 | def sync_source(source: Path, events: List[Event]) -> List[Any]:
17 | if not events:
18 | return []
19 | return sync_init(source)
20 |
21 |
22 | def _delete_recursive(path: Path, ignore_errors):
23 | for e in path.iterdir():
24 | if e.is_file():
25 | e.unlink(missing_ok=ignore_errors)
26 | elif e.is_dir():
27 | shutil.rmtree(e, ignore_errors=ignore_errors)
28 |
29 |
30 | def sync_target(target_root: Path, changes: List[Any]) -> None:
31 | if not changes:
32 | return
33 | _delete_recursive(target_root, ignore_errors=True)
34 | _delete_recursive(target_root, ignore_errors=False)
35 |
36 | b = base64.b64decode(changes[0])
37 | with BytesIO(b) as stream:
38 | with zipfile.ZipFile(stream, "r") as zip_file:
39 | zip_file.extractall(target_root)
40 |
41 |
42 | def sync_init(source: Path) -> List[Any]:
43 | b = _zip_in_memory(source)
44 | s = base64.b64encode(b).decode('utf-8')
45 | return [s]
46 |
47 |
48 | def _zip_path(zip_file, path):
49 | if os.path.isfile(path):
50 | zip_file.write(path, os.path.basename(path))
51 | elif os.path.isdir(path):
52 | for root, dirs, files in os.walk(path):
53 | for file in files:
54 | file_path = os.path.join(root, file)
55 | arcname = os.path.relpath(file_path, path)
56 | zip_file.write(file_path, arcname)
57 |
58 |
59 | def _zip_in_memory(path) -> bytes:
60 | stream = BytesIO()
61 | with zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=1) as zip_file:
62 | _zip_path(zip_file, path)
63 |
64 | stream.seek(0)
65 | return stream.getbuffer().tobytes()
66 |
--------------------------------------------------------------------------------
/src/wwwpy/server/proxy.py:
--------------------------------------------------------------------------------
1 | from wwwpy.common.rpc.serializer import RpcRequest
2 | from wwwpy.websocket import SendEndpoint
3 |
4 |
5 | class Proxy:
6 | def __init__(self, module_name: str, send_endpoint: SendEndpoint):
7 | self.send_endpoint = send_endpoint
8 | self.module_name = module_name
9 |
10 | def dispatch(self, func_name: str, *args) -> None:
11 | rpc_request = RpcRequest.to_json(self.module_name, func_name, *args)
12 | self.send_endpoint.send(rpc_request)
13 |
--------------------------------------------------------------------------------
/src/wwwpy/server/pytestlib/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib
2 |
3 | required = {
4 | ('pytest',),
5 | ('xvirt', 'pytest-xvirt'),
6 | ('playwright',),
7 | ('pytest_playwright', 'pytest-playwright')
8 | }
9 | missing_pip = set()
10 | installed = set()
11 | for tup in required:
12 |
13 | pip_name = tup[0] if len(tup) == 1 else tup[1]
14 | try:
15 | importlib.import_module(tup[0])
16 | installed.add(pip_name)
17 | except ImportError:
18 | missing_pip.add(pip_name)
19 |
20 | if len(missing_pip) > 0:
21 | msg = ('You need to install the following packages to use this plugin:\n '
22 | + ', '.join(missing_pip)
23 | + '\n\nYou can install them by running:\n `pip install ' + ' '.join(missing_pip) +
24 | '`\n\n'
25 | + 'Packages already installed: ' + ', '.join(installed)
26 | )
27 | raise ImportError(msg)
28 |
--------------------------------------------------------------------------------
/src/wwwpy/server/pytestlib/pytest_plugin.py:
--------------------------------------------------------------------------------
1 | # wwwpy/pytest_plugin.py
2 |
3 | import pytest
4 |
5 | from .playwrightlib import playwright_setup_page_logger
6 | from .xvirt_impl import XVirtImpl
7 |
8 |
9 | def pytest_addoption(parser):
10 | parser.addoption("--headful", action="store_true", default=False, help="run tests in headfull mode")
11 |
12 |
13 | def pytest_configure(config):
14 | # This function is called before any tests or fixtures are collected.
15 | # You can add any setup code here.
16 |
17 | # For example, you can add your conftest.py setup code here.
18 | # Note: You might need to adjust this code to work in this context.
19 | from .playwrightlib import playwright_patch_timeout
20 | playwright_patch_timeout()
21 |
22 |
23 | @pytest.hookimpl
24 | def pytest_xvirt_setup(config):
25 | import wwwpy.base_conf
26 | headful = config.getoption("--headful") or wwwpy.base_conf.PLAYWRIGHT_HEADFUL
27 | return XVirtImpl(headless=not headful)
28 |
29 |
30 | def pytest_unconfigure(config):
31 | # This function is called after all tests and fixtures have been collected.
32 | # You can add any teardown code here.
33 | pass
34 |
35 |
36 | @pytest.fixture(autouse=True)
37 | def before_each_after_each(request):
38 | if 'page' not in request.node.fixturenames:
39 | yield
40 | return
41 | page = request.getfixturevalue('page')
42 | playwright_setup_page_logger(page)
43 | yield
44 |
--------------------------------------------------------------------------------
/src/wwwpy/server/pytestlib/remote_conftest.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import pytest
4 | from js import window
5 |
6 | from wwwpy.common.fetch_debug import fetch_debug
7 |
8 |
9 | @pytest.fixture(scope="session")
10 | def event_loop():
11 | # this prevents pytest-asyncio to closing the pyodide event loop (webloop)
12 | yield asyncio.get_running_loop()
13 |
14 |
15 | def pytest_sessionstart(session):
16 | print(f'invocation_dir={session.config.invocation_dir}')
17 | print(f'rootpath={session.config.rootpath}')
18 |
19 |
20 | def pytest_xvirt_send_event(event_json):
21 | async def callback():
22 | path = '#xvirt_notify_path_marker#'
23 | await async_fetch_str(path, method='POST', data=event_json)
24 |
25 | asyncio.create_task(callback())
26 |
27 |
28 | async def async_fetch_str(url: str, method: str = 'GET', data: str = '') -> str:
29 | print(__name__ + ' ' + fetch_debug(url, method, data))
30 | response = await window.fetch(url, method=method, body=data)
31 | text = await response.text()
32 | return text
33 |
34 |
35 | def pytest_runtest_setup(item):
36 | _clean_doc_now()
37 |
38 |
39 | # intent_manager_tests.py fails without the following, don't know why
40 | def pytest_runtest_teardown(item, nextitem):
41 | _clean_doc_now()
42 |
43 |
44 | def _clean_doc_now():
45 | from wwwpy.remote import remote_fixtures
46 | remote_fixtures.clean_document_now()
47 |
--------------------------------------------------------------------------------
/src/wwwpy/server/pytestlib/remote_test_main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | from pathlib import Path
4 |
5 |
6 | async def main(rootpath, invocation_dir, args):
7 | from wwwpy.common.designer import log_emit
8 | def emit(msg: str):
9 | print(msg)
10 | from js import console
11 | console.log(msg)
12 |
13 | log_emit.add_once(emit)
14 | log_emit.warning_to_log()
15 | logging.getLogger().setLevel(logging.DEBUG)
16 | # from wwwpy.remote.designer import log_redirect
17 | # log_redirect.redirect_logging()
18 |
19 | from js import console
20 | console.log(f'main({rootpath}, {invocation_dir}, {args})')
21 | from wwwpy.common.tree import print_tree
22 | print_tree('/wwwpy_bundle')
23 |
24 | Path('/wwwpy_bundle/pytest.ini').write_text("[pytest]\n"
25 | "asyncio_mode = auto")
26 | from wwwpy.remote import micropip_install
27 | await micropip_install('pytest==7.2.2') # didn't work with update to 8.1.1
28 | await micropip_install('pytest-asyncio')
29 | await micropip_install('pytest-xvirt')
30 | import wwwpy.common.designer as des
31 | for package in des.pypi_packages:
32 | await micropip_install(package)
33 | import pytest
34 | print('-=-' * 20 + 'pytest imported')
35 |
36 | os.chdir(invocation_dir)
37 | pytest.main(args)
38 |
--------------------------------------------------------------------------------
/src/wwwpy/server/rpc4tests.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 |
4 | from tests.timeouts import timeout_multiplier
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | async def rpctst_echo(msg: str) -> str:
10 | return f'echo {msg}'
11 |
12 |
13 | async def rpctst_exec(source: str, timeout_secs: int = 1) -> None:
14 | """Example:
15 | page.mouse.click(100, 100)
16 | page.locator('input').fill('foo1')
17 |
18 | This method queue the command to be executed in the playwright thread,
19 | it waits the completion but do not return the result.
20 | """
21 | from wwwpy.server.pytestlib.xvirt_impl import xvirt_instances
22 | from wwwpy.server.pytestlib.playwrightlib import PlaywrightBunch
23 | i = len(xvirt_instances)
24 | assert i == 1, f'len(xvirt_instances)={i}'
25 | xvirt = xvirt_instances[0]
26 | args = xvirt.playwright_args
27 | assert args is not None
28 | pwb: PlaywrightBunch = args.instance
29 | assert pwb is not None
30 | gl = {'page': pwb.page, 'pwb': pwb}
31 | loop = asyncio.get_running_loop()
32 | done = asyncio.Event()
33 | ex = []
34 |
35 | def _execute():
36 | try:
37 | exec(source, gl)
38 | except Exception as e:
39 | logger.error(f'Error executing source: {e}')
40 | ex.append(e)
41 | loop.call_soon_threadsafe(done.set)
42 |
43 | args.queue.put(_execute)
44 | timeout = timeout_secs * timeout_multiplier()
45 | try:
46 | await asyncio.wait_for(done.wait(), timeout=timeout)
47 | except asyncio.TimeoutError:
48 | raise TimeoutError(f"Execution timed out after {timeout} seconds for code `{source}`")
49 |
50 | if ex: raise ex[0]
51 |
--------------------------------------------------------------------------------
/src/wwwpy/server/settingslib.py:
--------------------------------------------------------------------------------
1 | from .. import platformdirs
2 | from ..common.settingslib import Settings
3 |
4 |
5 | def user_settings() -> Settings:
6 | _user_config_path = platformdirs.user_config_path('wwwpy', roaming=True, ensure_exists=True)
7 | settings = Settings()
8 | settings.load(_user_config_path / 'settings.ini')
9 | return settings
10 |
--------------------------------------------------------------------------------
/src/wwwpy/server/tcp_port.py:
--------------------------------------------------------------------------------
1 | import errno
2 | import logging
3 | import socket
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 | _start = (10035 - 1) # windows doesn't like the previous port range
8 | _end = 30000
9 |
10 |
11 | def find_port() -> int:
12 | global _start
13 | while _start < _end:
14 | _start += 1
15 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
16 | try:
17 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
18 | s.bind(('0.0.0.0', _start))
19 | print(f'findport() -> {_start}')
20 | return _start
21 | except socket.error as e:
22 | if e.errno != errno.EADDRINUSE:
23 | raise
24 |
25 | raise Exception(f'find_port(start={_start},end={_end}) no bindable tcp port found in interval')
26 |
27 | if __name__ == '__main__':
28 | print(find_port())
29 | print(find_port())
30 |
31 |
32 | def is_port_busy(port: int) -> bool:
33 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
34 | try:
35 | if hasattr(socket, "SO_REUSEPORT"):
36 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
37 | s.bind(('0.0.0.0', port))
38 | return False
39 | except socket.error as e:
40 | # logger.error(f'is_port_busy({port}) -> {e.errno}', exc_info=e)
41 | if e.errno == errno.EADDRINUSE:
42 | return True
43 | raise
44 |
--------------------------------------------------------------------------------
/src/wwwpy/server/wait_url.py:
--------------------------------------------------------------------------------
1 | import urllib.error
2 | import urllib.request
3 | from time import sleep
4 |
5 |
6 | def wait_url(url: str) -> None:
7 | for _ in range(300):
8 | try:
9 | urllib.request.urlopen(url)
10 | return
11 | except urllib.error.HTTPError:
12 | return
13 | except Exception:
14 | sleep(0.01)
15 | raise Exception(f'timeout wait_url(`{url}`)')
16 |
--------------------------------------------------------------------------------
/src/wwwpy/unasync.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import sys
3 | import traceback
4 | from inspect import iscoroutinefunction
5 | from threading import Thread
6 |
7 |
8 | def unasync(f):
9 | if not iscoroutinefunction(f):
10 | return f
11 | result = None
12 | exc_info = None
13 | exception = None
14 |
15 | async def main_safe(*args, **kwargs):
16 | nonlocal result, exc_info, exception
17 | result = None
18 | exc_info = None
19 | exception = None
20 | try:
21 | result = await f(*args, **kwargs)
22 | except Exception as ex:
23 | exception = ex
24 | exc_info = sys.exc_info()
25 |
26 | def start(*args, **kwargs):
27 | asyncio.run(main_safe(*args, **kwargs))
28 |
29 | def wrapper(*args, **kwargs):
30 | nonlocal result, exc_info, exception
31 | thread = Thread(target=start, args=args, kwargs=kwargs)
32 | thread.start()
33 | thread.join()
34 | if exc_info is not None:
35 | exc_type, exc_value, exc_traceback = exc_info
36 | print('', file=sys.stderr)
37 | traceback.print_exception(exc_type, exc_value, exc_traceback, file=sys.stderr)
38 | raise exception
39 | return result
40 |
41 | return wrapper
42 |
--------------------------------------------------------------------------------
/src/wwwpy/webservers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy/webservers/__init__.py
--------------------------------------------------------------------------------
/src/wwwpy/webservers/asgi_webserver.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, Awaitable
2 |
3 | from wwwpy.common.asynclib import OptionalCoroutine
4 | from wwwpy.http import HttpRoute
5 | from wwwpy.server.asgi import routes_to_asgi_application, AsgiApplication
6 | from wwwpy.webserver import Webserver, Route
7 | from wwwpy.websocket import WebsocketRoute
8 |
9 |
10 | class AsgiWebserver(Webserver):
11 | def __init__(self, start):
12 | super().__init__()
13 | self._start = start
14 | self.app = AsgiApplication()
15 |
16 | def _setup_route(self, route: Route):
17 | if isinstance(route, HttpRoute):
18 | self.app.http_route[route.path] = route
19 | elif isinstance(route, WebsocketRoute):
20 | self.app.websocket_route[route.path] = route
21 | else:
22 | raise Exception(f'Unknown route type: {type(route)}')
23 |
24 | async def _start_listen(self) -> None:
25 | await self._start(self)
26 |
--------------------------------------------------------------------------------
/src/wwwpy/webservers/available_webservers.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from typing import Iterator, List
3 |
4 | from ..webserver import Webserver
5 |
6 |
7 | class AvailableWebservers:
8 | def __init__(self) -> None:
9 | self._classes = _webservers_classes()
10 |
11 | @property
12 | def ids(self) -> Iterator[str]:
13 | return map(lambda w: w.__name__, self._classes)
14 |
15 | def new_instance(self) -> Webserver:
16 | return self._classes[0]()
17 |
18 | def instances(self) -> Iterator[Webserver]:
19 | for webserver_class in self._classes:
20 | yield webserver_class()
21 |
22 |
23 | def _webservers_classes() -> List[type[Webserver]]:
24 | result: List[type[Webserver]] = []
25 | try:
26 | from .tornado import WsTornado
27 | result.append(WsTornado)
28 | except:
29 | pass
30 |
31 | return result
32 |
33 |
34 | _available_webservers: AvailableWebservers | None = None
35 |
36 |
37 | def available_webservers() -> AvailableWebservers:
38 | global _available_webservers
39 | if _available_webservers is None:
40 | _available_webservers = AvailableWebservers()
41 | return _available_webservers
42 |
--------------------------------------------------------------------------------
/src/wwwpy_plugins/README.md:
--------------------------------------------------------------------------------
1 | Namespace package for wwwpy plugins.
--------------------------------------------------------------------------------
/src/wwwpy_plugins/wwwpy/README.md:
--------------------------------------------------------------------------------
1 | The namespace `wwwpy_plugins.wwwpy` and all packages starting with `wwwpy_plugins.wwwpy-` are reserved for internal use.
--------------------------------------------------------------------------------
/src/wwwpy_plugins/wwwpy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/src/wwwpy_plugins/wwwpy/__init__.py
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from functools import partial
3 | from typing import Iterable
4 |
5 | import pytest
6 |
7 | from wwwpy.server.tcp_port import find_port
8 | from wwwpy.webserver import Webserver
9 | from wwwpy.webservers.available_webservers import available_webservers
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def _webservers_instances() -> Iterable[Webserver]:
15 | for i in available_webservers().instances():
16 | i.set_port(find_port())
17 | yield i
18 |
19 |
20 | def for_all_webservers():
21 | return partial(pytest.mark.parametrize, 'webserver', _webservers_instances(),
22 | ids=available_webservers().ids)()
23 |
24 |
--------------------------------------------------------------------------------
/tests/available_webservers_test.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterator
2 |
3 | from wwwpy.webservers.available_webservers import available_webservers
4 |
5 |
6 | def test_at_least_one():
7 | next(available_webservers().ids)
8 |
9 |
10 | def test_ids():
11 | for ws_id in available_webservers().ids:
12 | assert isinstance(ws_id, str)
13 | assert ws_id.isidentifier()
14 |
15 |
16 | def test_new_instance():
17 | instance = available_webservers().new_instance()
18 | assert instance is not None
19 |
20 |
21 | def test_instances():
22 | instances = available_webservers().instances()
23 | assert isinstance(instances, Iterator)
24 |
--------------------------------------------------------------------------------
/tests/common/collectionlib_test.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | from wwwpy.common import collectionlib as cl
4 |
5 |
6 | @dataclass
7 | class Item:
8 | name: str
9 | color: str
10 |
11 |
12 | class MyListMap(cl.ListMap[Item]):
13 | def _key(self, item: Item) -> str:
14 | return item.name
15 |
16 |
17 | def test_add_item():
18 | # GIVEN
19 | target = MyListMap()
20 |
21 | # WHEN
22 | target.append(Item('apple', 'red'))
23 |
24 | # THEN
25 | assert len(target) == 1
26 | assert target.get('apple').color == 'red'
27 |
28 | def test_insert_item():
29 | # GIVEN
30 | target = MyListMap()
31 |
32 | # WHEN
33 | target.insert(0, Item('apple', 'red'))
34 |
35 | # THEN
36 | assert len(target) == 1
37 | assert target.get('apple').color == 'red'
38 |
39 | def test_extend_items():
40 | # GIVEN
41 | target = MyListMap()
42 |
43 | # WHEN
44 | target.extend([Item('apple', 'red'), Item('banana', 'yellow')])
45 |
46 | # THEN
47 | assert len(target) == 2
48 | assert target.get('apple').color == 'red'
49 | assert target.get('banana').color == 'yellow'
50 |
51 | def test_items_in_constructor():
52 | # GIVEN
53 | target = MyListMap([Item('apple', 'red'), Item('banana', 'yellow')])
54 |
55 | # THEN
56 | assert len(target) == 2
57 | assert target.get('apple').color == 'red'
58 | assert target.get('banana').color == 'yellow'
59 | assert target.get('missing') is None
60 |
61 |
62 | def test_keyfunc_in_constructor():
63 | # GIVEN
64 | target = cl.ListMap([Item('apple', 'red'), Item('banana', 'yellow')], key_func=lambda x: x.color)
65 |
66 | # THEN
67 | assert len(target) == 2
68 | assert target.get('red').name == 'apple'
69 | assert target.get('yellow').name == 'banana'
70 |
71 |
72 | def test_equality_with_list():
73 | # GIVEN
74 | target = cl.ListMap(['a', 'b', 'c'])
75 |
76 | # THEN
77 | assert target == ['a', 'b', 'c']
78 |
--------------------------------------------------------------------------------
/tests/common/databind/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/common/databind/__init__.py
--------------------------------------------------------------------------------
/tests/common/designer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/common/designer/__init__.py
--------------------------------------------------------------------------------
/tests/common/designer/canvas_selection_test.py:
--------------------------------------------------------------------------------
1 | from wwwpy.common.designer.canvas_selection import CanvasSelection, CanvasSelectionChangeEvent
2 | from wwwpy.common.designer.html_locator import html_to_node_path
3 | from wwwpy.common.designer.locator_lib import Locator, Origin
4 |
5 |
6 | def _new_element_path_old():
7 | node_path = html_to_node_path("""""", [0])
8 | ep = Locator('p1.comp1', 'Comp2', node_path, Origin.source)
9 | return ep
10 |
11 |
12 | def _new_element_path(class_module, class_name, html, path, origin):
13 | return Locator(class_module, class_name, html_to_node_path(html, path), origin)
14 |
15 |
16 | _some_element_path = _new_element_path('p1.comp1', 'Comp2', "
", [0], Origin.source)
17 |
18 |
19 | def test_initial_state():
20 | # GIVEN
21 | target = CanvasSelection()
22 |
23 | # WHEN
24 |
25 | # THEN
26 | assert target.current_selection is None
27 |
28 |
29 | def test_assignment_should_stick():
30 | # GIVEN
31 | target = CanvasSelection()
32 | ep = _new_element_path_old()
33 |
34 | # WHEN
35 | target.current_selection = ep
36 |
37 | # THEN
38 | assert target.current_selection is ep
39 |
40 |
41 | def test_events():
42 | # GIVEN
43 | target = CanvasSelection()
44 | ep1 = _new_element_path_old()
45 | ep2 = _new_element_path('p1.comp1', 'Comp2', "
", [0], Origin.source)
46 | events = []
47 | target.on_change.add(lambda e: events.append(e))
48 |
49 | # WHEN
50 | target.current_selection = ep1
51 |
52 | # THEN
53 | assert events == [CanvasSelectionChangeEvent(None, ep1)]
54 | events.clear()
55 |
56 | # WHEN
57 | target.current_selection = ep2
58 |
59 | # THEN
60 | assert events == [CanvasSelectionChangeEvent(ep1, ep2)]
61 | events.clear()
62 |
63 | # WHEN
64 | target.current_selection = None
65 |
66 | # THEN
67 | assert events == [CanvasSelectionChangeEvent(ep2, None)]
68 |
--------------------------------------------------------------------------------
/tests/common/designer/component_fixture.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 | from tests.common import DynSysPath, dyn_sys_path
6 |
7 |
8 | # todo should be merged/refactored with TargetFixture
9 | class ComponentFixture:
10 | def __init__(self, dyn_sys_path: DynSysPath):
11 | self._source = None
12 | self.dyn_sys_path: DynSysPath = dyn_sys_path
13 |
14 | def write_component(self, path: str, class_name: str, html: str = '') -> Path:
15 | return self.dyn_sys_path.write_module2(path, f'''
16 | class {class_name}:
17 | def init_component(self):
18 | self.element.innerHTML = """{html}"""
19 | ''')
20 |
21 |
22 | @pytest.fixture
23 | def component_fixture(dyn_sys_path: DynSysPath):
24 | return ComponentFixture(dyn_sys_path)
25 |
--------------------------------------------------------------------------------
/tests/common/designer/element_library_test.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | import logging
3 |
4 | import pytest
5 |
6 | from wwwpy.common.designer import element_library
7 | from wwwpy.common.designer.element_library import ElementDef
8 | from wwwpy.common.rpc import serialization
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | @pytest.fixture
14 | def el():
15 | return element_library.element_library()
16 |
17 |
18 | def test_element_library(el):
19 | assert len(el.elements) > 0
20 |
21 |
22 | def test_basic_attributes(el):
23 | span = el.by_tag_name('span')
24 | assert span
25 | assert span.attributes.get('class') is not None
26 |
27 |
28 | def test_shown_element(el):
29 | assert el.by_tag_name('sl-button') is not None
30 |
31 |
32 | def test_serialization(el):
33 | for e in el.elements:
34 | e = dataclasses.replace(e)
35 | e.gen_html = None
36 | logger.debug(f'serializing {e.tag_name}')
37 | ser = serialization.to_json(e, ElementDef)
38 | deser = serialization.from_json(ser, ElementDef)
39 | assert e == deser
40 |
41 |
42 | def test_unknown_element(el):
43 | ed = el.by_tag_name('unknown-element')
44 | assert ed is not None
45 | assert ed is el.by_tag_name('unknown-element')
46 |
--------------------------------------------------------------------------------
/tests/common/designer/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/common/designer/ui/__init__.py
--------------------------------------------------------------------------------
/tests/common/designer/ui/_drop_indicator_svg_test.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as ET
2 |
3 | from wwwpy.common.designer.html_edit import Position
4 | from wwwpy.common.designer.ui._drop_indicator_svg import svg_indicator_for, position_for
5 |
6 |
7 | def test_valid_xml():
8 | for p in Position:
9 | # xml_string = 'text'
10 | xml_string = svg_indicator_for(100, 50, p)
11 | root = ET.fromstring(xml_string)
12 | assert root.tag == '{http://www.w3.org/2000/svg}svg'
13 |
14 |
15 | def test_svg_should_be_different_from_each_other():
16 | a, b, c, d = map(lambda p: svg_indicator_for(100, 50, p), Position)
17 | assert a != b
18 | assert a != c
19 | assert b != c
20 | assert a != d
21 | assert b != d
22 | assert c != d
23 |
24 |
25 | class TestPositionFor:
26 | def test_beforebegin(self):
27 | assert position_for(100, 50, 1, 1) == Position.beforebegin
28 |
29 | def test_afterend(self):
30 | assert position_for(100, 50, 100 - 1, 50 - 1) == Position.afterend
31 |
32 | def test_afterbegin(self):
33 | assert position_for(50, 50, 25 - 1, 25 - 1) == Position.afterbegin
34 |
35 | def test_beforeend(self):
36 | assert position_for(50, 50, 25 + 1, 25 + 1) == Position.beforeend
37 |
--------------------------------------------------------------------------------
/tests/common/designer/ui/rect_readonly_py_test.py:
--------------------------------------------------------------------------------
1 | from wwwpy.common.designer.ui.rect_readonly_py import RectReadOnlyPy
2 |
3 |
4 | def test_rect_read_only_dc_instantiation_and_properties():
5 | rect = RectReadOnlyPy(x=1.0, y=2.0, width=3.0, height=4.0)
6 | # base properties
7 | assert rect.x == 1.0
8 | assert rect.y == 2.0
9 | assert rect.width == 3.0
10 | assert rect.height == 4.0
11 | # calculated edges
12 | assert rect.left == rect.x
13 | assert rect.top == rect.y
14 | assert rect.right == rect.x + rect.width
15 | assert rect.bottom == rect.y + rect.height
16 |
17 |
18 | def test_rect_read_only_dc_to_json():
19 | rect = RectReadOnlyPy(x=1.0, y=2.0, width=3.0, height=4.0)
20 | json_data = rect.toJSON()
21 |
22 | # Should match DOMRectReadOnly.toJSON() structure
23 | assert json_data == {
24 | "x": 1.0,
25 | "y": 2.0,
26 | "width": 3.0,
27 | "height": 4.0
28 | }
29 |
30 |
31 | def test_rect_read_only_init_from_instance():
32 | original = RectReadOnlyPy(x=1.0, y=2.0, width=3.0, height=4.0)
33 | dup = RectReadOnlyPy(original)
34 | assert dup.x == 1.0
35 | assert dup.y == 2.0
36 | assert dup.width == 3.0
37 | assert dup.height == 4.0
38 | assert dup.left == original.left
39 | assert dup.top == original.top
40 | assert dup.right == original.right
41 | assert dup.bottom == original.bottom
42 |
43 |
44 | def test_rect_read_only_init_from_duck_typed_obj():
45 | class Dummy:
46 | def __init__(self, x, y, width, height):
47 | self.x = x
48 | self.y = y
49 | self.width = width
50 | self.height = height
51 |
52 | dummy = Dummy(5.0, 6.0, 7.0, 8.0)
53 | rect = RectReadOnlyPy(dummy)
54 | assert rect.x == 5.0
55 | assert rect.y == 6.0
56 | assert rect.width == 7.0
57 | assert rect.height == 8.0
58 | assert rect.left == 5.0
59 | assert rect.top == 6.0
60 | assert rect.right == 12.0
61 | assert rect.bottom == 14.0
62 | assert rect.toJSON() == {"x": 5.0, "y": 6.0, "width": 7.0, "height": 8.0}
63 |
--------------------------------------------------------------------------------
/tests/common/designer/ui/svg_test.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as ET
2 |
3 | from wwwpy.common.designer.ui.svg import build_document, add_rounded_background2
4 |
5 | # language=html
6 | _svg1 = """
7 |
8 |
12 | """
13 |
14 |
15 | def test_namespace_ok():
16 | svg_str = add_rounded_background2(_svg1, '#00FF00')
17 | assert 'ns0:' not in svg_str
18 | assert 'xmlns="http://www.w3.org/2000/svg"' in svg_str
19 |
20 |
21 | def test_change_stroke_color():
22 | def mutator(attrs):
23 | if 'stroke' in attrs:
24 | attrs['stroke'] = '#000000'
25 |
26 | result = build_document(_svg1, mutator)
27 | root = ET.fromstring(result)
28 | for elem in root.iter():
29 | if elem.tag.endswith('path'):
30 | assert elem.attrib['stroke'] == '#000000'
31 |
32 | assert 'ns0:' not in result
33 | assert 'xmlns="http://www.w3.org/2000/svg"' in result
34 |
35 |
36 | def test_add_data_test_attribute():
37 | def mutator(attrs):
38 | attrs['data-test'] = '1'
39 |
40 | result = build_document(_svg1, mutator)
41 | root = ET.fromstring(result)
42 | for elem in root.iter():
43 | assert elem.attrib.get('data-test') == '1'
44 |
45 | assert 'ns0:' not in result
46 | assert 'xmlns="http://www.w3.org/2000/svg"' in result
47 |
--------------------------------------------------------------------------------
/tests/common/event_observer_test.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 | from wwwpy.common.event_observer import EventObserver
4 |
5 |
6 | def test_not_enough_time_passed_for_stability():
7 | now = datetime(2021, 1, 1, 0, 0, 0)
8 | target = EventObserver(100, lambda: now)
9 | assert not target.is_stable()
10 |
11 |
12 | def test_enough_time_passed_for_stability():
13 | now = datetime(2021, 1, 1, 0, 0, 0)
14 |
15 | def time_provider():
16 | nonlocal now
17 | return now
18 |
19 | target = EventObserver(100, time_provider)
20 | now = now + timedelta(milliseconds=101)
21 | assert target.is_stable()
22 |
23 | def test_event_delay_stability():
24 | now = datetime(2021, 1, 1, 0, 0, 0)
25 |
26 | def time_provider():
27 | nonlocal now
28 | return now
29 |
30 | target = EventObserver(100, time_provider)
31 | now = now + timedelta(milliseconds=50)
32 | target.event_happened()
33 | now = now + timedelta(milliseconds=70)
34 | assert not target.is_stable()
35 | now = now + timedelta(milliseconds=70)
36 | assert target.is_stable()
37 |
--------------------------------------------------------------------------------
/tests/common/exitlib_test.py:
--------------------------------------------------------------------------------
1 | from wwwpy.common.exitlib import on_exit
2 |
3 |
4 | def test_on_exit_callback_triggered():
5 | calls = []
6 |
7 | def fun():
8 | on_exit(lambda: calls.append(1))
9 |
10 | fun()
11 |
12 | assert calls == [1]
13 |
14 |
15 | def test_multiple_on_exit_callbacks():
16 | calls = []
17 |
18 | def fun():
19 | on_exit(lambda: calls.append(1))
20 | on_exit(lambda: calls.append(2))
21 |
22 | fun()
23 |
24 | assert calls == [1, 2]
25 |
26 |
27 | def test_exception_on_exit():
28 | calls = []
29 |
30 | def fun():
31 | on_exit(lambda: calls.append(1))
32 | raise Exception("Test exception")
33 |
34 | try:
35 | fun()
36 | except Exception:
37 | pass
38 |
39 | assert calls == [1]
40 |
--------------------------------------------------------------------------------
/tests/common/iterlib_test.py:
--------------------------------------------------------------------------------
1 | from typing import Iterable, TypeVar, Generic, Callable, Iterator
2 |
3 | from wwwpy.common import iterlib
4 |
5 |
6 | def test_callable_to_iterable():
7 | def call():
8 | yield 1
9 | yield 2
10 |
11 | target = iterlib.CallableToIterable(call)
12 |
13 | assert list(target) == [1, 2]
14 | assert list(target) == [1, 2]
15 |
16 |
17 | def test_should_swallow_exception__during_creation():
18 | def call():
19 | raise Exception('some')
20 |
21 | target = iterlib.CallableToIterable(call)
22 |
23 | assert list(target) == []
24 | assert list(target) == []
25 |
26 |
27 | def test_should_swallow_exception__during_iteration():
28 | def call():
29 | yield 1
30 | raise Exception('some')
31 |
32 | target = iterlib.CallableToIterable(call)
33 |
34 | assert list(target) == [1]
35 | assert list(target) == [1]
36 |
--------------------------------------------------------------------------------
/tests/common/loglib_test.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from wwwpy.common.loglib import translate_names
4 |
5 |
6 | def test_translate_names():
7 | result = translate_names({'mod1.mod2': 'DEBUG', 'mod3': 'INFO'})
8 | assert result == {'mod1.mod2': logging.DEBUG, 'mod3': logging.INFO}
9 |
10 |
11 | def test_translate_unrecognized():
12 | result = translate_names({'mod1.mod2': 'DEBUG', 'mod3': 'INFO', 'mod4': 'FOO'})
13 | assert result == {'mod1.mod2': logging.DEBUG, 'mod3': logging.INFO}
14 |
--------------------------------------------------------------------------------
/tests/common/modlib_test.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from tests.common import restore_sys_path, dyn_sys_path, DynSysPath
4 | from wwwpy.common.modlib import _find_module_path
5 |
6 |
7 | def test_package_path(tmp_path, restore_sys_path):
8 | # GIVEN
9 | sys.path.insert(0, str(tmp_path))
10 | mod1py = tmp_path / 'module1.py'
11 | mod1py.write_text(' invalid python ')
12 |
13 | # WHEN
14 | target = _find_module_path('module1')
15 | # THEN
16 | assert target is not None
17 | assert str(mod1py) == str(target)
18 |
19 |
20 | def test_package_path2(dyn_sys_path: DynSysPath):
21 | # GIVEN
22 | mod1py = dyn_sys_path.write_module('package1', 'module1', ' I am module1')
23 | dyn_sys_path.write_module('package1', '__init__', 'I am package1')
24 |
25 | # WHEN
26 | target = _find_module_path('package1.module1')
27 | # THEN
28 | assert target is not None
29 | assert str(mod1py) == str(target)
30 |
31 |
32 | def test_package_path3(dyn_sys_path: DynSysPath):
33 | # GIVEN
34 | dyn_sys_path.write_module('p1', 'p2', 'import xyz')
35 | mod1py= dyn_sys_path.write_module('p1.p2', 'm1', 'import xyz')
36 |
37 | # WHEN
38 | target = _find_module_path('p1.p2.m1')
39 | # THEN
40 | assert target is not None
41 | assert str(mod1py) == str(target)
42 |
43 | def test_package_path4(dyn_sys_path: DynSysPath):
44 | # GIVEN
45 |
46 | # WHEN
47 | target = _find_module_path('wwwpy.remote.component')
48 | # THEN
49 | assert target is not None
50 | assert target.name == 'component.py'
51 |
52 | def test_package_not_found():
53 | # GIVEN
54 |
55 | # WHEN
56 | target = _find_module_path('package_not_exists')
57 | # THEN
58 | assert target is None
--------------------------------------------------------------------------------
/tests/common/quickstart_test.py:
--------------------------------------------------------------------------------
1 | from wwwpy.common.quickstart import quickstart_list, is_empty_project
2 |
3 |
4 | def test_quickstart_list():
5 | target = quickstart_list()
6 | assert target is not None
7 | assert len(target) > 0
8 |
9 |
10 | def test_basic():
11 | target = quickstart_list()
12 | basic = target.get('basic')
13 | assert basic is not None
14 | assert basic.name == 'basic'
15 | assert basic.title == 'Basic setup for a new project'
16 | assert basic.description == 'It contains just a simple Component with almost no content'
17 |
18 |
19 | def test_basic_should_be_the_first():
20 | target = quickstart_list()
21 | first = target[0]
22 | assert first.name == 'basic'
23 |
24 |
25 | def test_empty_project(tmp_path):
26 | assert is_empty_project(tmp_path)
27 |
28 |
29 | def test_empty_project2(tmp_path):
30 | (tmp_path / 'remote').mkdir()
31 | (tmp_path / '__pycache__').mkdir()
32 | assert is_empty_project(tmp_path)
33 |
34 |
35 | def test_empty_project3(tmp_path):
36 | (tmp_path / 'remote').mkdir()
37 | (tmp_path / 'remote/__init__.py').touch()
38 | assert is_empty_project(tmp_path)
39 |
40 |
41 | def test_not_empty_project(tmp_path):
42 | remote = tmp_path / 'remote'
43 | remote.mkdir()
44 | (remote / '__init__.py').write_text('# some content')
45 | assert not is_empty_project(tmp_path)
46 |
47 |
48 | def test_not_empty_project2(tmp_path):
49 | (tmp_path / 'some-file.txt').touch()
50 | assert not is_empty_project(tmp_path)
51 |
--------------------------------------------------------------------------------
/tests/common/reload/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/common/reload/__init__.py
--------------------------------------------------------------------------------
/tests/common/reload/reload_1/component.py:
--------------------------------------------------------------------------------
1 | print('loading 123...')
2 |
3 | class Class1:
4 | def __init__(self):
5 | self.a = 123
6 |
--------------------------------------------------------------------------------
/tests/common/reload/reload_1/reload_1_test_disable.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 |
4 | def test_1():
5 | import component
6 |
7 | assert component.Class1().a == 123
8 |
9 | # beware of issue https://stackoverflow.com/questions/71544388/understanding-pythons-importlib-reload
10 | component_file = (Path(__file__).parent / 'component.py')
11 | component_file.write_text(component_file.read_text().replace('.a = 123', '.b = 45'))
12 |
13 | from wwwpy.common import reloader
14 | reloader.reload(component)
15 |
16 | assert component.Class1().b == 45
17 |
--------------------------------------------------------------------------------
/tests/common/reload/reload_2/package2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/common/reload/reload_2/package2/__init__.py
--------------------------------------------------------------------------------
/tests/common/reload/reload_2/package2/class_a.py:
--------------------------------------------------------------------------------
1 | print('loading ClassA... 123')
2 | import package2.class_b as class_b
3 |
4 |
5 | class ClassA:
6 | def __init__(self):
7 | self.b = 45
8 | self.class_b = class_b.ClassB()
9 |
--------------------------------------------------------------------------------
/tests/common/reload/reload_2/package2/class_b.py:
--------------------------------------------------------------------------------
1 | print('loading ClassB 123...')
2 |
3 |
4 | class ClassB:
5 | def __init__(self):
6 | self.b = 45
7 |
--------------------------------------------------------------------------------
/tests/common/reload/reload_2/reload_2_test_disable.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from wwwpy.common import modlib
4 | from wwwpy.common.modlib import _find_module_path
5 | from wwwpy.common.reloader import unload_path
6 |
7 | parent = Path(__file__).parent / 'package2'
8 |
9 |
10 | def test_2():
11 | replace((parent / 'class_a.py'), '.b = 45', '.a = 123')
12 | replace((parent / 'class_b.py'), '.b = 45', '.a = 123')
13 |
14 | import package2.class_a
15 |
16 | assert package2.class_a.ClassA().a == 123
17 | assert package2.class_a.ClassA().class_b.a == 123
18 |
19 | # beware of issue https://stackoverflow.com/questions/71544388/understanding-pythons-importlib-reload
20 | replace((parent / 'class_a.py'), '.a = 123', '.b = 45')
21 | replace((parent / 'class_b.py'), '.a = 123', '.b = 45')
22 |
23 | p2 = modlib._find_package_directory('package2')
24 |
25 | unload_path(str(p2))
26 |
27 | del package2.class_a
28 | import package2.class_a
29 | assert package2.class_a.ClassA().b == 45
30 | assert package2.class_a.ClassA().class_b.b == 45
31 |
32 |
33 | def replace(path, old, new):
34 | path.write_text(path.read_text().replace(old, new))
35 |
--------------------------------------------------------------------------------
/tests/common/reload/reload_test.py:
--------------------------------------------------------------------------------
1 | """
2 | Reload tests are defined like the following:
3 | - setup initial module(s)
4 | - verify something about the module(s)
5 | - change the module(s)
6 | - invoke the reload function
7 | - verify the change(s) took effect
8 | """
9 | from pathlib import Path
10 |
11 |
12 | def test_reload_1(pytester):
13 | # GIVEN
14 | # copy all the content of test_files to the pytester path
15 | test_files = Path(__file__).parent / 'reload_1'
16 | _copy(test_files, pytester)
17 |
18 | # WHEN
19 | result = pytester.runpytest('-p', 'no:wwwpy')
20 | result.assert_outcomes(passed=1)
21 |
22 |
23 | def test_reload_2(pytester):
24 | """Class A uses class B. When class A is reloaded, class B should be reloaded as well."""
25 | # GIVEN
26 | # copy all the content of test_files to the pytester path
27 | test_files = Path(__file__).parent / 'reload_2'
28 | _copy(test_files, pytester)
29 |
30 | # WHEN
31 | result = pytester.runpytest('-p', 'no:wwwpy')
32 | result.assert_outcomes(passed=1)
33 |
34 |
35 | _ignore = {'__pycache__'}
36 |
37 |
38 | def _copy(test_files, pytester, subpath: str = ''):
39 | for file in test_files.iterdir():
40 | if file.is_dir():
41 | if file.name not in _ignore:
42 | _copy(file, pytester, file.name + '/')
43 | continue
44 | filename = file.name.replace('_test_disable.py', '_test.py')
45 | path = pytester.path / (subpath + filename)
46 | path.parent.mkdir(parents=True, exist_ok=True)
47 | path.write_text(file.read_text())
48 |
--------------------------------------------------------------------------------
/tests/common/rpc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/common/rpc/__init__.py
--------------------------------------------------------------------------------
/tests/common/rpc/custom_loader_test.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import sys
3 |
4 | from tests.common import dyn_sys_path, DynSysPath
5 | from wwwpy.common.rpc.custom_loader import CustomFinder
6 |
7 |
8 | def test_remote_rpc_interceptor(dyn_sys_path: DynSysPath):
9 | """Importing remote.rpc should not raise an exception even from the server side
10 | even though it imports 'js' that does not exist on the server side.
11 | It is because the import process of such package is handled and modified"""
12 | factory = MockLoaderFactory()
13 | target = CustomFinder({'remote.rpc'}, factory.MockLoader)
14 | sys.meta_path.insert(0, target)
15 | dyn_sys_path.write_module2('remote/rpc.py', ' not even python code')
16 | import remote # noqa
17 | assert remote
18 |
19 | import remote.rpc
20 | assert remote.rpc
21 |
22 | assert len(factory.mock_loaders) == 1
23 | assert factory.mock_loaders[0].sources == [' not even python code']
24 |
25 |
26 | class MockLoaderFactory:
27 | def __init__(self):
28 | self.mock_loaders = []
29 | parent = self
30 |
31 | class MockLoader(importlib.abc.Loader):
32 | def __init__(self, loader):
33 | self.loader = loader
34 | self.sources = []
35 | parent.mock_loaders.append(self)
36 |
37 | def create_module(self, spec):
38 | return None
39 |
40 | def exec_module(self, module):
41 | module_name = module.__name__
42 | source = self.loader.get_source(module_name)
43 | self.sources.append(source)
44 |
45 | self.MockLoader = MockLoader
46 |
--------------------------------------------------------------------------------
/tests/common/rpc/support1.py:
--------------------------------------------------------------------------------
1 | def support1_function0(a: int, b: int) -> int:
2 | pass
3 |
4 |
5 | async def support1_function1(a: int, # this is done on purpose
6 | b: float) -> str:
7 | pass
8 |
--------------------------------------------------------------------------------
/tests/common/rpc/support2.py:
--------------------------------------------------------------------------------
1 | def support2_mul(a: int, b: int) -> int:
2 | return a * b
3 |
4 |
5 | async def support2_concat(a: str, b: str) -> str:
6 | return a + b
7 |
--------------------------------------------------------------------------------
/tests/common/rpc/support3.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 |
3 |
4 | async def support3_mul(a: int, b: int) -> int:
5 | return a * b
6 |
7 |
8 | async def support3_concat(a: str, b: str) -> str:
9 | return a + b
10 |
11 |
12 | async def support3_with_typing_import() -> Dict[str, str]:
13 | return {'foo': 'bar'}
14 |
15 |
16 | async def support3_default_values_primitive_types(a: int, b: int = 2, c: int = 3) -> int:
17 | return a * b * c
18 |
19 |
20 | async def support3_throws_error(exception_message: str, return_message: str) -> str:
21 | if exception_message != '':
22 | raise Exception('support3_throws_error: ' + exception_message)
23 | if return_message != '':
24 | return return_message
25 |
--------------------------------------------------------------------------------
/tests/common/rpc/test_invoker.py:
--------------------------------------------------------------------------------
1 | from tests.common.rpc.test_rpc import support2_module_name
2 | from wwwpy.common.rpc.func_registry import from_package_name
3 | from wwwpy.common.rpc.invoker import Invoker
4 |
5 |
6 | def test_simple_invoke():
7 | module = from_package_name(support2_module_name)
8 | target = Invoker(module)
9 | # THEN
10 | actual = target['support2_mul'].func(6, 7)
11 | assert actual == 42
12 |
13 |
--------------------------------------------------------------------------------
/tests/common/rpc/test_serializer.py:
--------------------------------------------------------------------------------
1 | from tests.common.rpc.test_rpc import support2_module_name
2 | from wwwpy.common.rpc.serializer import RpcRequest
3 |
4 |
5 | def test_rpc():
6 | request = RpcRequest.to_json(support2_module_name, 'support2_mul', 6, 7)
7 | restored = RpcRequest.from_json(request)
8 |
9 | assert restored.module == support2_module_name
10 | assert restored.func == 'support2_mul'
11 | assert restored.args == [6, 7]
12 |
--------------------------------------------------------------------------------
/tests/common/rpc2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/common/rpc2/__init__.py
--------------------------------------------------------------------------------
/tests/common/rpc2/encoder_decoder_test.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from wwwpy.common.rpc2.encoder_decoder import JsonEncoderDecoder
4 |
5 |
6 | def test_encoder_buffer():
7 | target = JsonEncoderDecoder()
8 |
9 | enc = target.encoder()
10 |
11 | enc.encode('ciao', str)
12 | enc.encode(123, int)
13 |
14 | assert enc.buffer
15 |
16 | dec = target.decoder(enc.buffer)
17 |
18 | assert 'ciao' == dec.decode(str)
19 | assert 123 == dec.decode(int)
20 |
21 | with pytest.raises(Exception):
22 | dec.decode(str)
23 |
--------------------------------------------------------------------------------
/tests/common/rpc2/transport_fake.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass, field
4 |
5 | import pytest
6 |
7 | from wwwpy.common.rpc2.transport import Transport
8 |
9 |
10 | class TransportFake(Transport):
11 |
12 | def __init__(self):
13 | self.recv_buffer = []
14 | self.send_buffer = []
15 | self.send_sync_callback = lambda: None
16 |
17 | async def empty(): pass
18 |
19 | self.send_async_callback = empty
20 |
21 | def send_sync(self, payload: str | bytes):
22 | self.send_buffer.append(payload)
23 | self.send_sync_callback()
24 |
25 | async def send_async(self, payload: str | bytes):
26 | self.send_buffer.append(payload)
27 | await self.send_async_callback()
28 |
29 | def recv_sync(self) -> str | bytes:
30 | return self._consume()
31 |
32 | def _consume(self):
33 | if len(self.recv_buffer) == 0:
34 | raise Exception('Buffer is empty')
35 | return self.recv_buffer.pop(0)
36 |
37 | async def recv_async(self) -> str | bytes:
38 | return self._consume()
39 |
40 |
41 | @dataclass
42 | class PairedTransport:
43 | client: TransportFake = field(default_factory=lambda: TransportFake())
44 | server: TransportFake = field(default_factory=lambda: TransportFake())
45 |
46 | def __post_init__(self):
47 | self.client.recv_buffer = self.server.send_buffer
48 | self.client.send_buffer = self.server.recv_buffer
49 |
50 |
51 | def test_client_send_sync():
52 | target = PairedTransport()
53 |
54 | target.client.send_sync('payload1')
55 |
56 | assert target.server.recv_sync() == 'payload1'
57 |
58 |
59 | def test_loop_transport_empty():
60 | target = PairedTransport()
61 | with pytest.raises(Exception):
62 | target.server.recv_sync()
63 |
64 | with pytest.raises(Exception):
65 | target.client.recv_sync()
66 |
--------------------------------------------------------------------------------
/tests/common/rpc2/typed_function_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import dataclasses
4 |
5 | from wwwpy.common.rpc2.typed_function import get_typed_function, TypedFunction
6 |
7 |
8 | class Car: ...
9 |
10 |
11 | def test_sync():
12 | def mock_sync(a: int, b: Car) -> float: ...
13 |
14 | target = get_typed_function(mock_sync)
15 |
16 | assert target == TypedFunction(__name__, 'mock_sync', [int, Car], float, False)
17 |
18 |
19 | def test_async():
20 | async def mock_async(a: int, b: Car) -> float: ...
21 |
22 | target = get_typed_function(mock_async)
23 |
24 | assert target == TypedFunction(__name__, 'mock_async', [int, Car], float, True)
25 |
26 |
27 | def test_none_type():
28 | def none_explicit(a: int) -> None: ...
29 |
30 | def none_implicit(a: int): ...
31 |
32 | expected = TypedFunction(__name__, 'none_explicit', [int], type(None), False)
33 | assert get_typed_function(none_explicit) == dataclasses.replace(expected, func_name='none_explicit')
34 | assert get_typed_function(none_implicit) == dataclasses.replace(expected, func_name='none_implicit')
35 |
--------------------------------------------------------------------------------
/tests/common/settingslib_test.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 | from wwwpy.common.settingslib import Settings
6 |
7 |
8 | class Fix:
9 | def __init__(self, tmp_path: Path):
10 | self.target = Settings()
11 | self.ini = tmp_path / 'foo.ini'
12 | self.tmp_path = tmp_path
13 |
14 | def write_load(self, content):
15 | self.ini.write_text(content)
16 | self.target.load(self.ini)
17 |
18 |
19 | @pytest.fixture
20 | def fix(tmp_path) -> Fix:
21 | return Fix(tmp_path)
22 |
23 |
24 | def test_hotreload_self_default(fix: Fix):
25 | assert not fix.target.hotreload_self
26 |
27 |
28 | def test_hotreload_self_set(fix: Fix):
29 | fix.write_load('[general]\nhotreload_self=true')
30 | assert fix.target.hotreload_self
31 |
32 |
33 | def test_open_url(fix: Fix):
34 | assert not fix.target.open_url_code
35 |
36 |
37 | def test_open_url_set(fix: Fix):
38 | fix.write_load("""[general]\nopen_url_code=import os; os.system("open '{url}')""")
39 | assert fix.target.open_url_code == """import os; os.system("open '{url}')"""
40 |
41 |
42 | def test_log_level_empty(fix: Fix):
43 | assert {} == fix.target.log_level
44 |
45 |
46 | def test_log_level(fix: Fix):
47 | fix.write_load("""[log_level]\nmod1.mod2=DEBUG\nmod3=INFO""")
48 | assert {'mod1.mod2': 'DEBUG', 'mod3': 'INFO'} == fix.target.log_level
49 |
50 |
51 | def _new_target(tmp_path, content: str = None):
52 | target = Settings()
53 | ini = tmp_path / 'foo.ini'
54 | if content:
55 | ini.write_text(content)
56 | target.load(ini)
57 | return target, ini
58 |
--------------------------------------------------------------------------------
/tests/common/version_test.py:
--------------------------------------------------------------------------------
1 | def test_version():
2 | import wwwpy
3 | assert wwwpy.__version__
4 | print(wwwpy.__version__)
5 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | pytest_plugins = ['pytester']
2 |
3 | import logging
4 |
5 |
6 | def pytest_configure():
7 | packages = ['tests', 'wwwpy', 'common', 'remote', 'server']
8 | for log_name in packages:
9 | logging.getLogger(log_name).setLevel(logging.DEBUG)
10 |
11 |
12 | def pytest_report_from_serializable(config, data):
13 | replace_pair = [
14 | (
15 | 'File "/wwwpy_bundle/tests/',
16 | 'File "tests/'
17 | ),
18 | (
19 | 'File "/wwwpy_bundle/wwwpy/',
20 | 'File "src/wwwpy/'
21 | ),
22 | ]
23 |
24 | def _replace_in_string(s: str) -> str:
25 | for old, new in replace_pair:
26 | s = s.replace(old, new)
27 | return s
28 |
29 | sections = data.get('sections', [])
30 | for section in sections:
31 | name: str = section[0]
32 | content: str = section[1]
33 | new_content = _replace_in_string(content)
34 | if new_content != content:
35 | section[1] = f'This stderr was modified by {__file__}\n{new_content}'
36 |
--------------------------------------------------------------------------------
/tests/layer/LAYERS.md:
--------------------------------------------------------------------------------
1 | # Test layers
2 |
3 | Tests are organized in layers. The next layer uses the api from the previous one.
4 |
5 | - layer 1 - http server
6 | - half-duplex communication aka http-requests/http-response
7 |
8 | - layer 2 - resources
9 | - Resource represents something (a file) to be transferred
10 | - from_filesystem(...) returns an Iterable[PathResource]
11 | - library_resources() returns the library itself
12 | - define the user resources to be sent
13 | - locate/guesstimate the user files/folders
14 | - ... todo investigate better ways to do it
15 | - ; see python packages: modulefinder
16 | -
17 | -
18 |
19 | - build_archive(Iterator[Resource]) returns the bytes of a zip archive
20 |
21 | - layer 3 - remote code execution; playwright needed from here on
22 | - plain python code remote execution - pyodide
23 | - bootstrap_routes(...) deliver zipped python files and execute them
24 | - zip_route to serve an application/zip
25 | - bootstrap_route to serve an html file that
26 | - bootstrap pyodide with the following python
27 | - download the zip, uncompress it, add to sys path, execute some main()
28 | - ... docs; limited lib support: wwwpy is not available, only pyodide
29 | - layer 4 - convention(s)
30 | - layer 5 - test client/server communication
31 | - half-duplex rpc using http-request/http-response coms
32 | - full-duplex rpc using websockets/long-polling coms
33 | - full-duplex communication aka websocket/long-polling
34 | - layer 6 - higher level functionality
35 | - ask browser to refresh
36 |
37 | Tests should be executed on all supported web servers.
--------------------------------------------------------------------------------
/tests/layer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/layer/__init__.py
--------------------------------------------------------------------------------
/tests/layer/layer_2_support/build_archive/simple/dir1/bar.txt:
--------------------------------------------------------------------------------
1 | #bar
--------------------------------------------------------------------------------
/tests/layer/layer_2_support/build_archive/simple/foo.txt:
--------------------------------------------------------------------------------
1 | #foo
--------------------------------------------------------------------------------
/tests/layer/layer_2_support/from_filesystem/one_file/foo.py:
--------------------------------------------------------------------------------
1 | # foo.py
2 |
--------------------------------------------------------------------------------
/tests/layer/layer_2_support/from_filesystem/relative_to/yes/yes.txt:
--------------------------------------------------------------------------------
1 | yes
--------------------------------------------------------------------------------
/tests/layer/layer_2_support/from_filesystem/resource_filter/yes/reject/foo.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/layer/layer_2_support/from_filesystem/resource_filter/yes/reject/foo.txt
--------------------------------------------------------------------------------
/tests/layer/layer_2_support/from_filesystem/resource_filter/yes/yes.txt:
--------------------------------------------------------------------------------
1 | yes
--------------------------------------------------------------------------------
/tests/layer/layer_4_support/convention_b/remote/__init__.py:
--------------------------------------------------------------------------------
1 | from js import document
2 |
3 | document.body.innerHTML = ''
4 |
--------------------------------------------------------------------------------
/tests/layer/layer_4_support/convention_c_async/remote/__init__.py:
--------------------------------------------------------------------------------
1 | async def main():
2 | from js import document
3 | document.body.innerHTML = ''
4 |
--------------------------------------------------------------------------------
/tests/layer/layer_4_support/convention_c_sync/remote/__init__.py:
--------------------------------------------------------------------------------
1 | def main():
2 | from js import document
3 | document.body.innerHTML = ''
4 |
--------------------------------------------------------------------------------
/tests/layer/layer_5_support/rpc_remote/remote/__init__.py:
--------------------------------------------------------------------------------
1 | import js
2 |
3 | js.document.body.innerHTML = 'ready'
4 |
--------------------------------------------------------------------------------
/tests/layer/layer_5_support/rpc_remote/remote/rpc.py:
--------------------------------------------------------------------------------
1 | from js import document
2 |
3 |
4 | class Layer5Rpc1:
5 |
6 | def set_body_inner_html(self, msg):
7 | document.body.innerHTML = msg
8 |
--------------------------------------------------------------------------------
/tests/layer/layer_5_support/rpc_server/remote/__init__.py:
--------------------------------------------------------------------------------
1 | from js import document
2 |
3 |
4 | async def main():
5 | from server.rpc import multiply
6 | res = await multiply(7, 6)
7 | document.body.innerHTML = str(res)
8 |
--------------------------------------------------------------------------------
/tests/layer/layer_5_support/rpc_server/server/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/layer/layer_5_support/rpc_server/server/__init__.py
--------------------------------------------------------------------------------
/tests/layer/layer_5_support/rpc_server/server/rpc.py:
--------------------------------------------------------------------------------
1 | async def multiply(a: int, b: int) -> int:
2 | return a * b
3 |
--------------------------------------------------------------------------------
/tests/layer/test_dev_mode.py:
--------------------------------------------------------------------------------
1 | import tests.server.convention_fixture
2 | import wwwpy.server.convention
3 | from tests.common import dyn_sys_path, DynSysPath
4 | from wwwpy.common.files import get_all_paths_with_hashes
5 |
6 |
7 | def test_dev_mode_disabled__should_NOT_create_canonical_components(dyn_sys_path: DynSysPath):
8 | tests.server.convention_fixture.start_test_convention(dyn_sys_path.path)
9 | assert get_all_paths_with_hashes(dyn_sys_path.path) == set()
10 |
11 |
12 | def test_dev_mode_non_empty_folder_but_no_remote__should_not_fail(dyn_sys_path: DynSysPath):
13 | # dyn_sys_path.path.mkdir('some-folder')
14 | (dyn_sys_path.path / 'some-folder').mkdir()
15 | tests.server.convention_fixture.start_test_convention(dyn_sys_path.path, dev_mode=True)
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/layer/test_layer_1.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import http.client
4 | import threading
5 | import urllib.parse
6 | from http import HTTPStatus
7 | from time import sleep
8 | from typing import Callable
9 |
10 | from tests import for_all_webservers
11 | from wwwpy.http import HttpRoute, HttpResponse, HttpRequest
12 | from wwwpy.server.tcp_port import find_port
13 | from wwwpy.server.fetch import sync_fetch_response
14 | from wwwpy.webserver import Webserver
15 |
16 |
17 | class TestHttpRoute:
18 |
19 | @for_all_webservers()
20 | def test_webservers_get(self, webserver: Webserver):
21 | response_a = HttpResponse('a', 'text/plain')
22 | response_b = HttpResponse('b', 'text/html')
23 |
24 | webserver.set_routes(HttpRoute('/b', lambda req, res: res(response_b)))
25 | webserver.set_routes(HttpRoute('/', lambda req, res: res(response_a)))
26 |
27 | webserver.set_port(find_port()).start_listen()
28 |
29 | url = webserver.localhost_url()
30 |
31 | assert sync_fetch_response(url) == response_a
32 | assert sync_fetch_response(url + '/b') == response_b
33 |
34 | @for_all_webservers()
35 | def test_webservers_post(self, webserver: Webserver):
36 | # GIVEN
37 | response_a = HttpResponse('a', 'text/plain')
38 | actual_request: HttpRequest | None = None
39 |
40 | def handler(req: HttpRequest, res) -> None:
41 | nonlocal actual_request
42 | actual_request = req
43 | # return response_a
44 | res(response_a)
45 |
46 | http_route = HttpRoute('/route1', handler)
47 |
48 | webserver.set_routes(http_route).start_listen()
49 |
50 | url = webserver.localhost_url()
51 |
52 | # WHEN
53 | actual_response = sync_fetch_response(url + '/route1', method='POST', data='post-body')
54 |
55 | # THEN
56 | assert actual_response == response_a
57 | assert actual_request.method == 'POST'
58 | assert actual_request.content.decode('utf8') == 'post-body'
59 |
--------------------------------------------------------------------------------
/tests/layer/test_layer_4.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from playwright.sync_api import Page, expect
4 |
5 | import tests.server.convention_fixture
6 | import wwwpy.server.convention
7 | from tests import for_all_webservers
8 | from tests.common import restore_sys_path
9 | from wwwpy.resources import from_directory
10 | from wwwpy.server import configure
11 | from wwwpy.webserver import Webserver
12 |
13 | file_parent = Path(__file__).parent
14 |
15 |
16 | @for_all_webservers()
17 | def test_server_convention_b(page: Page, webserver: Webserver, restore_sys_path):
18 | _test_convention('convention_b', page, webserver)
19 |
20 |
21 | @for_all_webservers()
22 | def test_server_convention_c_async(page: Page, webserver: Webserver, restore_sys_path):
23 | _test_convention('convention_c_async', page, webserver)
24 |
25 |
26 | @for_all_webservers()
27 | def test_server_convention_c_sync(page: Page, webserver: Webserver, restore_sys_path):
28 | _test_convention('convention_c_sync', page, webserver)
29 |
30 | sub_text = "This may be because the running directory is not a valid wwwpy project directory."
31 |
32 | @for_all_webservers()
33 | def test_extraneous_file(page: Page, webserver: Webserver, restore_sys_path, tmp_path: Path):
34 | tmp_path.touch('some_file.txt')
35 | tests.server.convention_fixture.start_test_convention(tmp_path, webserver)
36 | webserver.start_listen()
37 | page.goto(webserver.localhost_url())
38 | expect(page.locator("body")).to_contain_text(sub_text)
39 |
40 |
41 | @for_all_webservers()
42 | def test_empty__folder__error_message(page: Page, webserver: Webserver, restore_sys_path, tmp_path: Path):
43 | tests.server.convention_fixture.start_test_convention(tmp_path, webserver)
44 | webserver.start_listen()
45 | page.goto(webserver.localhost_url())
46 | expect(page.locator("body")).to_contain_text(sub_text)
47 |
48 |
49 | def _test_convention(directory, page, webserver):
50 | tests.server.convention_fixture.start_test_convention(file_parent / 'layer_4_support' / directory, webserver)
51 | webserver.start_listen()
52 | page.goto(webserver.localhost_url())
53 | expect(page.locator('id=tag1')).to_have_value(directory)
54 |
--------------------------------------------------------------------------------
/tests/remote/README.md:
--------------------------------------------------------------------------------
1 | This tests will be executed in the browser
--------------------------------------------------------------------------------
/tests/remote/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/remote/__init__.py
--------------------------------------------------------------------------------
/tests/remote/component_future_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from js import HTMLDivElement
4 |
5 | from wwwpy.remote.component import Component, element
6 |
7 |
8 | def test_correct_annotation():
9 | class Comp1(Component):
10 | div1: HTMLDivElement = element()
11 |
12 | def init_component(self):
13 | self.element.innerHTML = """div1
"""
14 |
15 | c = Comp1()
16 | nop = c.div1
17 |
--------------------------------------------------------------------------------
/tests/remote/databind/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/remote/databind/__init__.py
--------------------------------------------------------------------------------
/tests/remote/databind/test_databind.py:
--------------------------------------------------------------------------------
1 | """this should test only InputTargetAdapter directly, not the whole databind"""
2 | from __future__ import annotations
3 |
4 | import logging
5 | from dataclasses import dataclass
6 |
7 | import js
8 | import pytest
9 | from js import document
10 |
11 | from wwwpy.common.databind.databind import Binding
12 | from wwwpy.remote.databind.bind_wrapper import InputTargetAdapter
13 | from wwwpy.server.rpc4tests import rpctst_exec
14 |
15 | logger = logging.getLogger(__name__)
16 |
17 |
18 | @dataclass
19 | class User:
20 | name: str
21 |
22 |
23 | @dataclass
24 | class Car:
25 | color: str
26 |
27 |
28 | async def test_databind_input_string1(fixture: Fixture):
29 | # GIVEN
30 | tag1 = _new_target_adapter()
31 | user = User('foo1')
32 |
33 | # WHEN
34 | _bind(user, 'name', tag1)
35 |
36 | # THEN
37 | assert tag1.input.value == 'foo1'
38 |
39 |
40 | async def test_databind_input_string2(fixture: Fixture):
41 | # GIVEN
42 | tag1 = _new_target_adapter()
43 | car1 = Car('yellow')
44 |
45 | # WHEN
46 | _bind(car1, 'color', tag1)
47 |
48 | # THEN
49 | assert tag1.input.value == 'yellow'
50 |
51 |
52 | async def test_databind_input_string__target_to_source(fixture: Fixture):
53 | # GIVEN
54 | tag1 = _new_target_adapter()
55 | car1 = Car('')
56 |
57 | # WHEN
58 | _bind(car1, 'color', tag1)
59 |
60 | await rpctst_exec("page.locator('#tag1').press_sequentially('yellow1')")
61 |
62 | # THEN
63 | assert car1.color == 'yellow1'
64 |
65 |
66 | def _new_target_adapter(tag_id: str = 'tag1'):
67 | tag1: js.HTMLInputElement = document.createElement('input') # noqa
68 | tag1.id = tag_id
69 | document.body.append(tag1)
70 | return InputTargetAdapter(tag1)
71 |
72 |
73 | def _bind(instance, attr_name, target_adapter):
74 | target = Binding(instance, attr_name, target_adapter)
75 | target.apply_binding()
76 |
77 |
78 | class Fixture:
79 | pass
80 |
81 |
82 | @pytest.fixture
83 | def fixture():
84 | document.body.innerHTML = ''
85 | return Fixture()
86 |
--------------------------------------------------------------------------------
/tests/remote/designer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/remote/designer/__init__.py
--------------------------------------------------------------------------------
/tests/remote/designer/drop_zone_test.py:
--------------------------------------------------------------------------------
1 | from js import document, MouseEvent
2 | import js
3 | from wwwpy.remote import dict_to_js
4 | from wwwpy.remote.designer import drop_zone
5 |
6 |
7 | def test__extract_first_with_data_name():
8 | # GIVEN
9 | document.body.innerHTML = ''
10 | foo: js.HTMLDivElement = document.getElementById('foo')
11 | event = _materialize_mousemove(foo)
12 |
13 | # WHEN
14 | result = drop_zone._extract_first_with_data_name(event)
15 |
16 | # THEN
17 | assert result is not None
18 | assert result == foo.parentElement
19 |
20 |
21 | def test__the_element_itself_if_has_data_name():
22 | # GIVEN
23 | document.body.innerHTML = ''
24 | foo: js.HTMLDivElement = document.getElementById('foo')
25 | event = _materialize_mousemove(foo)
26 |
27 | # WHEN
28 | result = drop_zone._extract_first_with_data_name(event)
29 |
30 | # THEN
31 | assert result is not None
32 | assert result == foo
33 |
34 |
35 | def test__if_no_data_name_is_found__return_event_target():
36 | # GIVEN
37 | document.body.innerHTML = ''
38 | foo: js.HTMLDivElement = document.getElementById('foo')
39 | event = _materialize_mousemove(foo)
40 |
41 | # WHEN
42 | result = drop_zone._extract_first_with_data_name(event)
43 |
44 | # THEN
45 | assert result is not None
46 | assert result == foo
47 |
48 |
49 | def _materialize_mousemove(element):
50 | mouse_move_event = MouseEvent.new("mousemove", dict_to_js({
51 | "bubbles": True,
52 | "cancelable": True,
53 | "clientX": 150, # X coordinate of the mouse pointer
54 | "clientY": 250, # Y coordinate of the mouse pointer
55 | }))
56 | events = []
57 |
58 | def handle_real_event(event: MouseEvent):
59 | events.append(event)
60 |
61 | element.onmousemove = lambda ev: handle_real_event(ev)
62 | element.dispatchEvent(mouse_move_event)
63 | assert len(events) == 1
64 | event = events[0]
65 | return event
66 |
--------------------------------------------------------------------------------
/tests/remote/designer/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/remote/designer/ui/__init__.py
--------------------------------------------------------------------------------
/tests/remote/designer/ui/button_tab_test.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from js import document
3 | import js
4 | import logging
5 | from wwwpy.remote.designer.ui.button_tab import ButtonTab, Tab
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 | @pytest.fixture
10 | def target():
11 | target = ButtonTab()
12 | document.body.innerHTML = ''
13 | document.body.appendChild(target.element)
14 | try:
15 | yield target
16 | finally:
17 | document.body.innerHTML = ''
18 |
19 |
20 | def test_set_mixed_tabs(target):
21 | target.tabs = [Tab('Tab1'), 'Tab2']
22 |
23 | html = target.root_element().innerHTML
24 | assert 'Tab1' in html
25 | assert 'Tab2' in html
26 |
27 |
28 | def test_click_event(target):
29 | # GIVEN
30 | target.tabs = ['Tab1', 'Tab2']
31 | tab_click = []
32 |
33 | def on_selected(tab):
34 | tab_click.append(tab.text)
35 |
36 | for t in target._tabs:
37 | t.on_selected = on_selected
38 |
39 | # WHEN
40 | target.tabs[0].root_element().click()
41 | assert tab_click == ['Tab1']
42 |
43 | target.tabs[1].root_element().click()
44 | assert tab_click == ['Tab1', 'Tab2']
45 |
46 |
47 | def test_click_event_on_tab(target):
48 | # GIVEN
49 | tab_click = []
50 | target.tabs = [Tab('Tab1', on_selected=lambda tab: tab_click.append(tab.text))]
51 |
52 | # WHEN
53 | target.tabs[0].root_element().click()
54 | assert tab_click == ['Tab1']
55 |
56 |
57 | def test_selection(target):
58 | # GIVEN
59 | target.tabs = ['Tab1', 'Tab2']
60 |
61 | # WHEN
62 | tab1 = target.tabs[0]
63 | tab2 = target.tabs[1]
64 |
65 | logger.warning('follow with tab1.click()')
66 | tab1.root_element().click()
67 |
68 | assert tab1.selected
69 | assert not tab2.selected
70 |
71 | logger.warning('follow with tab2.click()')
72 | tab2.root_element().click()
73 |
74 | assert not tab1.selected, target.root_element().innerHTML
75 | assert tab2.selected, target.root_element().innerHTML
76 |
--------------------------------------------------------------------------------
/tests/remote/designer/ui/palette_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 |
5 | from pyodide.ffi import create_proxy
6 |
7 | from wwwpy.remote.designer.ui.palette import PaletteItemComponent
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | class TestPaletteItem:
12 |
13 | def test_selected_should_have_class_selected(self):
14 | item = PaletteItemComponent()
15 | item.selected = True
16 | assert item.element.classList.contains('selected')
17 |
18 | def test_selected_deselected_should_not_have_class_selected(self):
19 | item = PaletteItemComponent()
20 | item.selected = True
21 | item.selected = False
22 | assert not item.element.classList.contains('selected')
23 |
--------------------------------------------------------------------------------
/tests/remote/designer/ui/tool_selection_indicator_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import js
4 |
5 | from wwwpy.common.designer.ui.rect_readonly_py import RectReadOnlyPy
6 | from wwwpy.remote._elementlib import element_xy_center
7 | from wwwpy.remote.designer.ui.floater_selection_indicator import SelectionIndicatorFloater
8 |
9 |
10 | def test_simple():
11 | # GIVEN
12 | target = SelectionIndicatorFloater()
13 | rect = RectReadOnlyPy(22, 11, 40, 20)
14 | rect_center = rect.xy_center
15 | target.set_reference_geometry(rect)
16 |
17 | # WHEN
18 | js.document.body.append(target.element)
19 | element_center = element_xy_center(target.element)
20 |
21 | # THEN
22 | assert rect_center == element_center
23 |
24 | rect_area = rect.width * rect.height
25 | element_area = target.element.clientWidth * target.element.clientHeight
26 | assert rect_area * 0.9 < element_area < rect_area * 1.1
27 |
--------------------------------------------------------------------------------
/tests/remote/element_lib_test.py:
--------------------------------------------------------------------------------
1 | import js
2 |
3 | from wwwpy.remote._elementlib import ensure_tag_instance
4 |
5 |
6 | class Test_ensure_tag_instance:
7 | def test_in_body(self):
8 | element1 = ensure_tag_instance('div', 'test_id', js.document.body)
9 | element2 = ensure_tag_instance('div', 'test_id', js.document.body)
10 |
11 | assert element1 == element2
12 |
13 | def test_in_head(self):
14 | element1 = ensure_tag_instance('style', 'test_id', js.document.head)
15 | element2 = ensure_tag_instance('style', 'test_id', js.document.head)
16 |
17 | assert element1 == element2
18 |
--------------------------------------------------------------------------------
/tests/remote/rpc4tests_helper.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 | from typing import TYPE_CHECKING
5 |
6 | from wwwpy.server import rpc4tests
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | if TYPE_CHECKING:
11 | from playwright.sync_api import Page # noqa
12 |
13 |
14 | async def rpctst_exec(source: str | list[str], timeout_secs: int = 1) -> None:
15 | if isinstance(source, str):
16 | await _rpctst_exec(source, timeout_secs)
17 | elif isinstance(source, list):
18 | for cmd in source:
19 | await _rpctst_exec(cmd, timeout_secs)
20 | else:
21 | raise TypeError(f"source must be str or list[str], got {type(source)}")
22 |
23 |
24 | async def rpctst_exec_touch_event(events: dict | list[dict]) -> None:
25 | if isinstance(events, dict):
26 | events = [events]
27 | for event in events:
28 | cmd = f'pwb.cdp.send("Input.dispatchTouchEvent", {event})'
29 | logger.debug(f'rpctst_exec_touch_event: `{cmd}`')
30 | await _rpctst_exec(cmd)
31 |
32 |
33 | async def _rpctst_exec(source: str, timeout_secs: int = 1) -> None:
34 | logger.debug(f'rpctst_exec: `{source}` (timeout_secs={timeout_secs})')
35 | await rpc4tests.rpctst_exec(source, timeout_secs)
36 | # class PlaywrightPageCallable(Protocol):
37 | # def __call__(self, page: Page) -> None: ...
38 |
39 | # PlaywrightPageCallable = Callable[[Page], None]
40 |
41 | # def page_exec(block: Callable[[Page], None]):
42 | # pass
43 |
44 | # def main():
45 | # page_exec(lambda page: page.mouse.click(0, 0))
46 | # page_exec(lambda page: page.mouse.click(0, 0))
47 | #
48 | # def test_func(page: Page):
49 | # page.mouse.click(0, 0)
50 |
51 | # page_exec(test_func)
52 | #
53 | # if __name__ == '__main__':
54 | # main()
55 |
--------------------------------------------------------------------------------
/tests/remote/test_in_pyodide.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from js import document
3 |
4 |
5 | def test_first():
6 | document.body.innerHTML = ''
7 | assert document.getElementById('tag1').value == 'foo1'
8 |
9 |
10 | @pytest.mark.asyncio
11 | async def test_second():
12 | return
13 | assert False, 'This sadly succeeds, because async tests are running correctly in Pyodide' + \
14 | 'See issue: https://github.com/pyodide/pyodide/issues/2221#issuecomment-2379624269'
15 |
--------------------------------------------------------------------------------
/tests/remote/test_rpc.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import js
4 | from js import document
5 |
6 | import wwwpy.remote.component as wpc
7 | from wwwpy.server.rpc4tests import rpctst_exec
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | async def test_some():
13 | class Comp1(wpc.Component):
14 | button1: js.HTMLButtonElement = wpc.element()
15 |
16 | def init_component(self):
17 | self.element.innerHTML = """"""
18 | self.events = []
19 |
20 | def button1__click(self, event):
21 | self.events.append(event)
22 | self.button1.innerHTML += '.'
23 |
24 | comp1 = Comp1()
25 | document.body.innerHTML = ''
26 | document.body.append(comp1.element)
27 | for idx in range(1, 15):
28 | await rpctst_exec('page.mouse.click(100, 100)')
29 | assert len(comp1.events) == idx
30 | assert len(comp1.button1.innerHTML) == idx
31 | # await _assert_retry(lambda: len(comp1.events) == idx)
32 | # await _assert_retry(lambda: len(comp1.button1.innerHTML) == idx)
33 |
34 |
35 | # async def _assert_retry(condition):
36 | # source = inspect.getsource(condition).strip()
37 | # __tracebackhide__ = True
38 | # [await sleep(0.01) for _ in range(100) if not condition()]
39 | # assert condition(), f'wait_condition timeout `{source}`'
40 |
--------------------------------------------------------------------------------
/tests/server/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/server/__init__.py
--------------------------------------------------------------------------------
/tests/server/cmd_line_args_test.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 | import os
5 |
6 | from wwwpy.server.__main__ import parse_arguments, Arguments
7 |
8 | cwd_path = Path(os.getcwd())
9 |
10 |
11 | def test_default_arguments():
12 | args = parse_arguments([])
13 | assert args == Arguments(directory=cwd_path, port=8000, dev=False)
14 |
15 |
16 | def test_dev_mode():
17 | args = parse_arguments(['dev'])
18 | assert args == Arguments(directory=cwd_path, port=8000, dev=True)
19 |
20 |
21 | def test_dev_mode_with_custom_port():
22 | args = parse_arguments(['dev', '--port', '9000'])
23 | assert args == Arguments(directory=cwd_path, port=9000, dev=True)
24 |
25 |
26 | def test_custom_port():
27 | args = parse_arguments(['--port', '9000'])
28 | assert args == Arguments(directory=cwd_path, port=9000, dev=False)
29 |
30 |
31 | def test_custom_directory_and_port_with_dev():
32 | args = parse_arguments(['--directory', '/tmp', '--port', '1234', 'dev'])
33 | assert args == Arguments(directory=Path('/tmp').absolute(), port=1234, dev=True)
34 |
35 |
36 | def test_invalid_port():
37 | with pytest.raises(SystemExit):
38 | parse_arguments(['--port', 'invalid'])
39 |
40 |
41 | def test_help_option():
42 | with pytest.raises(SystemExit):
43 | parse_arguments(['--help'])
44 |
45 |
46 | def test_unknown_option():
47 | with pytest.raises(SystemExit):
48 | parse_arguments(['--unknown'])
49 |
--------------------------------------------------------------------------------
/tests/server/convention_fixture.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path
4 |
5 | from wwwpy.common.settingslib import Settings
6 | from wwwpy.server.configure import Project, setup
7 | from wwwpy.server.convention import default_config, add_project
8 | from wwwpy.webserver import Webserver
9 |
10 |
11 | def start_test_convention(directory: Path, webserver: Webserver = None, dev_mode=False) -> Project:
12 | config = default_config(directory, dev_mode)
13 | project = setup(config, Settings())
14 |
15 | if webserver is not None:
16 | webserver.set_routes(*project.routes)
17 |
18 | return project
19 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/server/filesystem_sync/__init__.py
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/activity_monitor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from datetime import timedelta, datetime
4 | from typing import Callable
5 |
6 |
7 | class ActivityMonitor:
8 |
9 | def __init__(self, window: timedelta, time_func: Callable[[], datetime] = datetime.utcnow):
10 | super().__init__()
11 | self._last_touch: datetime | None = None
12 | self.window = window
13 | self._time_func = time_func
14 |
15 | def at_rest(self) -> bool:
16 | delta = self.rest_delta()
17 | if delta is None:
18 | # print('at_rest: True')
19 | return True
20 | ar = delta >= self.window
21 | # print(f'at_rest: {ar}')
22 | return ar
23 |
24 | def rest_delta(self) -> timedelta | None:
25 | lt = self._last_touch
26 | if lt is None:
27 | return None
28 | delta = self._time_func() - lt
29 | return delta
30 |
31 | def touch(self):
32 | self._last_touch = self._time_func()
33 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/activity_monitor_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from datetime import timedelta
4 |
5 | import pytest
6 |
7 | from tests.server.filesystem_sync.activity_monitor import ActivityMonitor
8 | from tests.server.filesystem_sync.time_mock import TimeMock
9 |
10 | _time_mock = TimeMock()
11 |
12 |
13 | @pytest.fixture
14 | def target():
15 | _time_mock.reset()
16 | return ActivityMonitor(timedelta(milliseconds=20), time_func=_time_mock)
17 |
18 |
19 | def test_activity_monitor(target):
20 | assert target.at_rest()
21 |
22 |
23 | def test_touch__change_at_rest(target):
24 | target.touch()
25 | assert not target.at_rest()
26 | _time_mock.advance(timedelta(milliseconds=10))
27 | assert not target.at_rest()
28 |
29 |
30 | def test_rest_delta(target):
31 | assert target.rest_delta() is None
32 | target.touch()
33 | _time_mock.advance(timedelta(milliseconds=10))
34 | assert target.rest_delta() == timedelta(milliseconds=10)
35 |
36 |
37 | def test_touch_after_time_window(target):
38 | target.touch()
39 | _time_mock.advance(timedelta(milliseconds=21))
40 | assert target.at_rest()
41 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/event_logger.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from pathlib import Path
3 | from typing import List
4 |
5 | from watchdog.events import FileSystemEvent
6 |
7 |
8 | @dataclass(frozen=True, eq=True, order=True)
9 | class Event:
10 | event_type: str = field(compare=True)
11 | is_directory: bool = field(compare=True)
12 | src_path: str = field(compare=True)
13 | dest_path: str = field(compare=True)
14 |
15 |
16 | _events = set()
17 |
18 |
19 | def _to_event(e: FileSystemEvent) -> Event:
20 | event = Event(e.event_type, e.is_directory, e.src_path, e.dest_path)
21 | return event
22 |
23 |
24 | def _simplify(e: Event) -> Event:
25 | src_path = '' if e.src_path is '' else 'src'
26 | dest_path = '' if e.dest_path is '' else 'dest'
27 | return Event(e.event_type, e.is_directory, src_path, dest_path)
28 |
29 |
30 | def log_filesystem_events(events: List[FileSystemEvent]):
31 | try:
32 | for e in events:
33 | event = _simplify(_to_event(e))
34 | _events.add(event)
35 | sorted_events = sorted(_events)
36 | Path('/tmp/events.txt').write_text('\n'.join([str(e) for e in sorted_events]))
37 | except Exception as ex:
38 | pass
39 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/event_rebase_test.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from tests.server.filesystem_sync.filesystem_fixture import FilesystemFixture, fixture
4 | from wwwpy.common.filesystem import sync
5 | from wwwpy.common.filesystem.sync import event_rebase
6 |
7 |
8 | def test_root_file_should_not_fire_remote_notification(fixture: FilesystemFixture):
9 | with fixture.source_mutator as m:
10 | m.touch('foo.txt')
11 |
12 | actual = event_rebase.filter_by_directory(fixture.source_mutator.events, {'some'})
13 |
14 | assert actual == []
15 |
16 |
17 | def test_pertinent_change_should_be_included(fixture: FilesystemFixture):
18 | with fixture.source_mutator as m:
19 | m.write('foo.txt', 'content')
20 |
21 | actual = event_rebase.filter_by_directory(fixture.source_mutator.events, {''})
22 |
23 | assert sync.Event('modified', False, 'foo.txt') in actual
24 |
25 |
26 | def test_pertinent_multiple_change_should_be_included(fixture: FilesystemFixture):
27 | with fixture.source_mutator as m:
28 | m.write('foo.txt', 'content')
29 | m.mkdir('p1')
30 | m.mkdir('p2')
31 | m.write('p1/p1.txt', 'content')
32 | m.write('p2/p2.txt', 'content')
33 |
34 | actual = event_rebase.filter_by_directory(fixture.source_mutator.events, {'p1', 'p2'})
35 |
36 | assert sync.Event('modified', False, 'p1/p1.txt') in actual
37 | assert sync.Event('modified', False, 'p2/p2.txt') in actual
38 | assert sync.Event('modified', False, 'foo.txt') not in actual
39 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/time_mock.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 |
4 | class TimeMock:
5 | def __init__(self):
6 | self.now = datetime(2023, 1, 1, 0, 0, 0)
7 |
8 | def reset(self):
9 | self.now = datetime(2023, 1, 1, 0, 0, 0)
10 |
11 | def __call__(self):
12 | return self.now
13 |
14 | def advance(self, delta: timedelta):
15 | self.now += delta
16 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/wakeup_recorder.py:
--------------------------------------------------------------------------------
1 | class WakeupRecorder:
2 | def __init__(self):
3 | self.events = 0
4 |
5 | def call(self, debouncer):
6 | self.events += 1
7 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/watchdog-events.txt:
--------------------------------------------------------------------------------
1 | Event(event_type='closed', is_directory=False, src_path='src', dest_path='')
2 | Event(event_type='created', is_directory=False, src_path='src', dest_path='')
3 | Event(event_type='created', is_directory=True, src_path='src', dest_path='')
4 | Event(event_type='deleted', is_directory=False, src_path='src', dest_path='')
5 | Event(event_type='deleted', is_directory=True, src_path='src', dest_path='')
6 | Event(event_type='modified', is_directory=False, src_path='src', dest_path='')
7 | Event(event_type='modified', is_directory=True, src_path='src', dest_path='')
8 | Event(event_type='moved', is_directory=False, src_path='src', dest_path='dest')
9 | Event(event_type='moved', is_directory=True, src_path='src', dest_path='dest')
10 |
11 |
--------------------------------------------------------------------------------
/tests/server/filesystem_sync/watchdog_debouncer_test.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from pathlib import Path
3 | from time import sleep
4 | from typing import List
5 |
6 | from tests.timeouts import timeout_multiplier
7 | from wwwpy.common.filesystem.sync import Event
8 | from wwwpy.server.filesystem_sync.watchdog_debouncer import WatchdogDebouncer
9 |
10 |
11 | def test_basic_event_expectation(tmp_path: Path):
12 | """Testing on multiplatform watchdog events and behavior is very difficult. Let's keep it simple"""
13 | # GIVEN
14 | events = []
15 |
16 | def callback(evs: List[Event]):
17 | events.extend(evs)
18 |
19 | target = WatchdogDebouncer(tmp_path, timedelta(milliseconds=200 * timeout_multiplier()), callback)
20 | target.start()
21 | file_path = tmp_path / 'test_file.txt'
22 | file_path.touch()
23 |
24 | # WHEN
25 |
26 | # wait debouncing
27 | _wait_condition(lambda: target.debouncer.is_debouncing)
28 | _wait_condition(lambda: not target.debouncer.is_debouncing)
29 |
30 | # THEN
31 |
32 | assert events != [], f'events={events}'
33 |
34 |
35 | def _wait_condition(condition):
36 | __tracebackhide__ = True
37 | [sleep(0.1) for _ in range(5 * timeout_multiplier()) if not condition()]
38 | assert condition(), 'wait_condition timeout'
39 |
--------------------------------------------------------------------------------
/tests/server/pytestlib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/server/pytestlib/__init__.py
--------------------------------------------------------------------------------
/tests/server/pytestlib/xvirt_impl_test.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import sys
4 |
5 | import pytest
6 |
7 | # todo, incomplete, slow and difficult to setup
8 | def disabled_test_user_folders(pytester: pytest.Pytester):
9 | """Create remote package and verify that it is loaded by the browser"""
10 | src = os.environ['OLDPWD'] + '/.cache/ms-playwright'
11 | dst = os.path.expanduser('~/.cache/ms-playwright')
12 | shutil.copytree(src, dst)
13 | tr = pytester.mkpydir('tests')
14 | sys.path.insert(0, str(pytester.path))
15 | print(f'tr: {tr}')
16 | tr = pytester.mkpydir('tests/remote')
17 | (tr / 'some_test.py').write_text('def test_1(): pass')
18 |
19 | result = pytester.runpytest('--headful')
20 |
21 | # THEN
22 | result.assert_outcomes(passed=1, failed=0)
23 |
--------------------------------------------------------------------------------
/tests/server/remote_ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/server/remote_ui/__init__.py
--------------------------------------------------------------------------------
/tests/server/remote_ui/searchable_combobox_test.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import expect
2 |
3 | from tests import for_all_webservers
4 | from tests.server.page_fixture import PageFixture, fixture
5 |
6 |
7 | @for_all_webservers()
8 | def test_combo(fixture: PageFixture):
9 | # language=python
10 | fixture.start_remote("""
11 | from wwwpy.remote.designer.ui.searchable_combobox2 import SearchableComboBox
12 | import js
13 | from js import document
14 | target = SearchableComboBox()
15 | document.body.innerHTML = ''
16 | document.body.append(target.element)
17 | target.placeholder = 'target-placeholder1'
18 | """)
19 | page = fixture.page
20 |
21 | # expect(page.get_by_text("foo123")).to_be_attached()
22 | # await expect(page.locator('input#my-input')).toHaveValue('1');
23 | # expect(page.locator('input')).to_have_value('foo123')
24 | locator = page.get_by_placeholder("target-placeholder1")
25 | expect(locator).to_have_value("")
26 | fixture.evaluate("import remote")
27 | fixture.evaluate("remote.target.text_value = 'foo123'")
28 | expect(locator).to_have_value("foo123")
29 | return
30 | locator = page.locator('input[value=""]')
31 | expect(locator).to_have_count(1)
32 | return
33 | page.locator("#root").locator("")
34 | assert False, page.locator("#root").inner_html()
35 | # expect(page.locator('body')).to_have_js_property()
36 | # fixture.evaluate("import remote; remote.target.text_value = 'foo123'")
37 | # expect(page.locator('body')).to('ready')
38 | # assert False, page.locator('head').inner_html()
39 | # fixture.assert_evaluate("False, 'see if it shows'")
40 |
--------------------------------------------------------------------------------
/tests/server/rpc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/server/rpc/__init__.py
--------------------------------------------------------------------------------
/tests/server/rpc/test_invoker.py:
--------------------------------------------------------------------------------
1 | from tests.common.rpc.test_rpc import support2_module_name
2 | from wwwpy.common.rpc.func_registry import from_package_name
3 | from wwwpy.common.rpc.invoker import Invoker
4 | from wwwpy.unasync import unasync
5 |
6 |
7 | @unasync
8 | async def test_module_invoke_async():
9 | module = from_package_name(support2_module_name)
10 | target = Invoker(module)
11 |
12 | # THEN
13 | actual = await target['support2_concat'].func('hello', ' world')
14 | assert actual == 'hello world'
15 |
--------------------------------------------------------------------------------
/tests/server/rpc/test_rpc_sync.py:
--------------------------------------------------------------------------------
1 | from playwright.sync_api import expect
2 |
3 | from tests import for_all_webservers
4 | from tests.server.page_fixture import fixture, PageFixture
5 |
6 |
7 | @for_all_webservers()
8 | def test_sync_rpc_func(fixture: PageFixture):
9 | # GIVEN
10 | fixture.dev_mode = False
11 | fixture.write_module('server/rpc.py', "def func1() -> str: return 'ready'")
12 |
13 | # WHEN
14 | fixture.start_remote( # language=python
15 | """
16 | async def main():
17 | import js
18 | from server import rpc
19 | js.document.body.innerText = 'first=' + rpc.func1()
20 | """)
21 | # THEN
22 | expect(fixture.page.locator('body')).to_have_text('first=ready', use_inner_text=True)
23 |
--------------------------------------------------------------------------------
/tests/server/startup/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwpy-labs/wwwpy/2179159a90bd712a8507f67d667dbd28db9f7f74/tests/server/startup/__init__.py
--------------------------------------------------------------------------------
/tests/server/startup/test_dev_mode.py:
--------------------------------------------------------------------------------
1 | from wwwpy.server.tcp_port import find_port
2 |
3 |
4 | def test_start_dev_mode__empty_folder(tmp_path):
5 | from wwwpy.server.convention import start_default
6 | start_default(tmp_path, find_port(), dev_mode=True)
7 | assert True
8 |
--------------------------------------------------------------------------------
/tests/server/tcp_port_test.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import socket
3 |
4 | from wwwpy.server import tcp_port
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | def test_is_port_busy():
10 | port = tcp_port.find_port()
11 | assert not tcp_port.is_port_busy(port)
12 |
13 |
14 | def test_port_is_busy():
15 | port = tcp_port.find_port()
16 |
17 | with socket.create_server(('0.0.0.0', port)) as s:
18 | assert tcp_port.is_port_busy(port)
19 |
20 | assert not tcp_port.is_port_busy(port)
21 |
22 |
--------------------------------------------------------------------------------
/tests/test_unsync.py:
--------------------------------------------------------------------------------
1 | from wwwpy.unasync import unasync
2 |
3 |
4 | def test_unsync():
5 | @unasync
6 | async def fun():
7 | return 'ok'
8 |
9 | assert fun() == 'ok'
10 |
11 |
12 | def test_unsync_with_args():
13 | @unasync
14 | async def fun(arg1):
15 | return arg1
16 |
17 | assert fun('foo') == 'foo'
18 |
--------------------------------------------------------------------------------
/tests/timeouts.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | logger = logging.getLogger(__name__)
5 |
6 |
7 | def is_github():
8 | getenv = os.getenv('GITHUB_ACTIONS')
9 | return getenv == 'true'
10 |
11 |
12 | def timeout_multiplier():
13 | multiplier = 15 if is_github() else 1
14 | return multiplier
15 |
16 |
17 | logger.debug(f'timeout_multiplier={timeout_multiplier()}')
18 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | env_list = py310, py311, py312, py313
3 | isolated_build = True
4 |
5 | [testenv]
6 | extras = test
7 | passenv =
8 | PLAYWRIGHT_PATCH_TIMEOUT_MILLIS
9 | FORCE_COLOR
10 | GITHUB_ACTIONS
11 | commands =
12 | python -m playwright install chromium
13 | python -m playwright install-deps chromium
14 | python -m pytest -rP {posargs:--verbose --showlocals --log-level=DEBUG} tests/
15 |
16 | [testenv:py310]
17 | basepython = python3.10
18 |
19 | [testenv:py311]
20 | basepython = python3.11
21 |
22 | [testenv:py312]
23 | basepython = python3.12
24 |
25 | [testenv:py313]
26 | basepython = python3.13
27 |
--------------------------------------------------------------------------------