Compare commits

...

34 Commits

Author SHA1 Message Date
Madhur Tandon
156c23d550 Update README.md (#1872) 2023-11-28 15:53:23 +05:30
Andrea Giammarchi
30396ba79a Fix #1861 - Use addon to fit lines instead of truncating (#1867) 2023-11-27 15:05:23 +01:00
Andrea Giammarchi
a4343c62ca Updated dev/dependencies w/ polyscript & coincident (#1864) 2023-11-22 15:21:57 +01:00
Antonio Cuni
4b89c84692 use UTC time (#1859) 2023-11-15 16:00:09 +01:00
Antonio Cuni
df68449b82 Improve README and and mention the community calls (#1858)
Improve the readme in two ways:

- remove the mention to <py-repl>, and shows a quick summary of the various ways of running Python code

- add a link to the google calendar which contains the community calls
2023-11-15 12:10:52 +01:00
Andrea Giammarchi
48e3383f66 Fix #1841 - Provide a better error when input is used (#1857) 2023-11-14 15:25:17 +01:00
Fabio Pliger
e750fa7393 Add Deprecation message when loading from latest (#1848)
* add tests to verify if we show an error banner when users load from latest

* add deprecation manager to take care of showing a notification in case script src is being loaded from latest

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* make sure deprecation warning also register onWorker

* restore tests for banner in worker

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add a wait selector when testing banner since worker seems to take too long to render in CI

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix test_deprecate_loading_scripts_from_latest: I think that the
previous failure was because we actually TRIED to execute the js from
latest/core.js and it conflicted with our local copy.

But to trigger the warning is enough to have a script pointing to
pyscript.net/latest, there is no need to execute it: modify it with
type="ignore-me" and an URL which doesn't exist.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Antonio Cuni <anto.cuni@gmail.com>
2023-11-10 08:18:30 -08:00
Fabio Pliger
5a15199a3a Fix Click test example (#1849)
* fix type in div id

* add types
2023-11-08 11:09:15 -08:00
Fabio Pliger
1801472fc4 Remove when from pydom (#1850)
* bye bye when

* fix create to drop parent and actually pass the other arguments through

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update types

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-08 09:28:58 -08:00
Nicholas Tollervey
ab15ac37ff Add playwright to dependency installation steps. (#1852) 2023-11-08 13:31:12 +00:00
Nicholas Tollervey
0955a6be49 Re-add CHANGELOG.md into root of the repository. (#1851)
* Re-add CHANGELOG.md from the tip of "classic" into root of the repository. Tidy
the formatting in CHANGELOG.md. Update the PR template to reflect the new
location of the CHANGELOG.md.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Replace with specific version.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-08 13:28:12 +00:00
Jeff Glass
d58237ea15 Update link to not use /latest (#1847)
* Update link in README.me to use specific version, not /latest
2023-11-07 16:37:10 -06:00
Andrea Giammarchi
2d50ca86a6 Fix #1840 - Do not bootstrap interactive shell (#1846) 2023-11-07 19:22:08 +01:00
Andrea Giammarchi
f1a46be738 Fix #1838 - Provides all TS from all projects (#1843) 2023-11-07 17:17:40 +01:00
Andrea Giammarchi
3e2a67d434 PyTerminal: use Pyodide instead of Python (#1833) 2023-11-03 17:59:11 +01:00
Andrea Giammarchi
aef028be6e Fix #1834 - Throw an error if more than a terminal exists (#1837) 2023-11-03 15:19:30 +01:00
Andrea Giammarchi
c8ec29a3d8 Improve offline dist content (#1836) 2023-11-03 10:00:52 +01:00
Fabio Pliger
e81830a2ea Value property to PyDom.Element and ElementCollection (#1828)
* add base test for input value field

* add value property to Element

* add test for non supported element

* prevent users to set value attribute on elements that do not support it

* add test for setting value on collections

* add value property to collection and add more tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-01 08:33:38 -07:00
Nicholas Tollervey
54df7171a2 Update publish-snapshot.yml (#1827)
Ensure playwright is installed when building.
2023-11-01 10:23:33 +00:00
Andrea Giammarchi
b31af823d1 Use <py-terminal> as default target (#1826) 2023-10-31 16:58:20 +01:00
Andrea Giammarchi
72f266532b PyScript Terminal - the latest kind (#1816) 2023-10-31 15:16:15 +01:00
Andrea Giammarchi
d9bf5cae12 Fix #1814 - Basic mpy integration (#1815) 2023-10-27 15:30:21 +02:00
Andrea Giammarchi
cd95a42e5e Fix #1812 - Avoid duplicated pyscript module (#1813) 2023-10-26 19:44:31 +02:00
Andrea Giammarchi
e67eb06d8b Breaking: new Polyscript Hooks mechanism (#1811)
* Breaking: new Polyscript Hooks mechanism

* Added proper smoke test
2023-10-26 17:13:36 +02:00
Andrea Giammarchi
28d37cdead Fix #1775 - Use latest polyscript dev script (#1810) 2023-10-24 15:23:05 +02:00
Nicholas Tollervey
13604e0a47 Simplify Makefile. Remove Conda. Use requirements.txt. Remove pointless type annotations. Update CI tests.yml. (#1793) 2023-10-24 09:53:10 +01:00
Andrea Giammarchi
aeb6f1a755 Create a dist.zip artifact (#1809) 2023-10-24 10:29:29 +02:00
Madhur Tandon
92e6f711b7 fix docs for append as True (#1808) 2023-10-23 19:32:32 +05:30
Andrea Giammarchi
a24113f42b Fix #1799 - Avoid multiple bootstraps when embedded (#1800) 2023-10-23 15:54:51 +02:00
Madhur Tandon
7a6f8ab3ad Update README.md (#1806) 2023-10-23 19:06:29 +05:30
Andrea Giammarchi
6dd242f3ce Allow PyScript to fully run locally (#1805) 2023-10-20 15:02:05 +02:00
shubhalgupta
88fa82c61a Fix contributing link is README file #1782 (#1797) 2023-10-10 13:24:15 -05:00
Andrea Giammarchi
2299ba5f61 Updated npm version (#1796) 2023-10-10 15:23:18 +02:00
Andrea Giammarchi
117df6ca38 Updated to latest polyscript + coincident (#1795) 2023-10-10 12:38:01 +02:00
60 changed files with 2094 additions and 669 deletions

View File

@@ -11,5 +11,5 @@
<!-- Note: Only user-facing changes require a changelog entry. Internal-only API changes do not require a changelog entry. Changes in documentation do not require a changelog entry. -->
- [ ] All tests pass locally
- [ ] I have updated `docs/changelog.md`
- [ ] I have updated `CHANGELOG.md`
- [ ] I have created documentation for this(if applicable)

View File

@@ -35,7 +35,7 @@ jobs:
${{ runner.os }}-
- name: NPM Install
run: npm install
run: npm install && npx playwright install
- name: Build
run: npm run build

View File

@@ -37,7 +37,7 @@ jobs:
${{ runner.os }}-
- name: npm install
run: npm install
run: npm install && npx playwright install
- name: build
run: npm run build

View File

@@ -41,7 +41,7 @@ jobs:
${{ runner.os }}-
- name: Install Dependencies
run: npm install
run: npm install && npx playwright install
- name: Build Pyscript.core
run: npm run build

View File

@@ -42,7 +42,7 @@ jobs:
${{ runner.os }}-
- name: NPM Install
run: npm install
run: npm install && npx playwright install
- name: Build
run: npm run build

View File

@@ -57,15 +57,24 @@ jobs:
- name: setup Miniconda
uses: conda-incubator/setup-miniconda@v2
- name: Setup Environment
run: make setup
- name: Create and activate virtual environment
run: |
python3 -m venv test_venv
source test_venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
echo VIRTUAL_ENV=$VIRTUAL_ENV >> $GITHUB_ENV
- name: Setup dependencies in virtual environment
run: |
make setup
- name: Build
run: make build
- name: Integration Tests
#run: make test-integration-parallel
run: make test-integration
run: |
make test-integration
- uses: actions/upload-artifact@v3
with:

4
.gitignore vendored
View File

@@ -144,6 +144,8 @@ test_results
# @pyscript/core npm artifacts
pyscript.core/core.*
pyscript.core/dist
pyscript.core/dist
pyscript.core/dist.zip
pyscript.core/src/plugins.js
pyscript.core/src/stdlib/pyscript.js
pyscript.core/src/3rd-party/*
!pyscript.core/src/3rd-party/READMEmd

View File

@@ -42,7 +42,7 @@ repos:
rev: "v3.0.0-alpha.6"
hooks:
- id: prettier
exclude: pyscript\.core/test|pyscript\.core/dist|pyscript\.core/types|pyscript.core/src/stdlib/pyscript.js|pyscript\.sw/
exclude: pyscript\.core/test|pyscript\.core/dist|pyscript\.core/types|pyscript.core/src/stdlib/pyscript.js|pyscript\.sw/|pyscript.core/src/3rd-party
args: [--tab-width, "4"]
- repo: https://github.com/pycqa/isort

87
CHANGELOG.md Normal file
View File

@@ -0,0 +1,87 @@
# Release Notes
## 2023.05.01
### Features
- Added the `xterm` attribute to `py-config`. When set to `True` or `xterm`, an (output-only) [xterm.js](http://xtermjs.org/) terminal will be used in place of the default py-terminal.
- The default version of Pyodide is now `0.23.2`. See the [Pyodide Changelog](https://pyodide.org/en/stable/project/changelog.html#version-0-23-2) for a detailed list of changes.
- Added the `@when` decorator for attaching Python functions as event handlers
- The `py-mount` attribute on HTML elements has been deprecated, and will be removed in a future release.
#### Runtime py- attributes
- Added logic to react to `py-*` attributes changes, removal, `py-*` attributes added to already live nodes but also `py-*` attributes added or defined via injected nodes (either appended or via `innerHTML` operations). ([#1435](https://github.com/pyscript/pyscript/pull/1435))
#### &lt;script type="py"&gt;
- Added the ability to optionally use `<script type="py">`, `<script type="pyscript">` or `<script type="py-script">` instead of a `<py-script>` custom element, in order to tackle cases where the content of the `<py-script>` tag, inevitably parsed by browsers, could accidentally contain _HTML_ able to break the surrounding page layout. ([#1396](https://github.com/pyscript/pyscript/pull/1396))
#### &lt;py-terminal&gt;
- Added a `docked` field and attribute for the `<py-terminal>` custom element, enabled by default when the terminal is in `auto` mode, and able to dock the terminal at the bottom of the page with auto scroll on new code execution.
#### &lt;py-script&gt;
- Restored the `output` attribute of `py-script` tags to route `sys.stdout` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
- Added a `stderr` attribute of `py-script` tags to route `sys.stderr` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
#### &lt;py-repl&gt;
- The `output` attribute of `py-repl` tags now specifies the id of the DOM element that `sys.stdout`, `sys.stderr`, and the results of a REPL execution are written to. It no longer affects the location of calls to `display()`
- Added a `stderr` attribute of `py-repl` tags to route `sys.stderr` to a DOM element with the given ID. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
- Resored the `output-mode` attribute of `py-repl` tags. If `output-mode` == 'append', the DOM element where output is printed is _not_ cleared before writing new results.
- Load code from the attribute src of py-repl and preload it into the corresponding py-repl tag by use the attribute `str` in your `py-repl` tag([#1292](https://github.com/pyscript/pyscript/pull/1292))
- &lt;py-repl&gt; elements now have a `getPySrc()` method, which returns the code inside the REPL as a string.([#1516](https://github.com/pyscript/pyscript/pull/1292))
#### Plugins
- Plugins may now implement the `beforePyReplExec()` and `afterPyReplExec()` hooks, which are called immediately before and after code in a `py-repl` tag is executed. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
#### Web worker support
- introduced the new experimental `execution_thread` config option: if you set `execution_thread = "worker"`, the python interpreter runs inside a web worker
- worker support is still **very** experimental: not everything works, use it at your own risk
### Bug fixes
- Fixes [#1280](https://github.com/pyscript/pyscript/issues/1280), which describes the errors on the PyRepl tests related to having auto-gen tags that shouldn't be there.
### Enhancements
- Py-REPL tests now run on both osx and non osx OSs
- migrated from _rollup_ to _esbuild_ to create artifacts
- updated `@codemirror` dependency to its latest
### Docs
- Add docs for event handlers
## 2023.03.1
### Features
### Bug fixes
- Fixed an issue where `pyscript` would not be available when using the minified version of PyScript. ([#1054](https://github.com/pyscript/pyscript/pull/1054))
- Fixed missing closing tag when rendering an image with `display`. ([#1058](https://github.com/pyscript/pyscript/pull/1058))
- Fixed a bug where Python plugins methods were being executed twice. ([#1064](https://github.com/pyscript/pyscript/pull/1064))
### Enhancements
- When adding a `py-` attribute to an element but didn't added an `id` attribute, PyScript will now generate a random ID for the element instead of throwing an error which caused the splash screen to not shutdown. ([#1122](https://github.com/pyscript/pyscript/pull/1122))
- You can now disable the splashscreen by setting `enabled = false` in your `py-config` under the `[splashscreen]` configuration section. ([#1138](https://github.com/pyscript/pyscript/pull/1138))
### Documentation
- Fixed 'Direct usage of document is deprecated' warning in the getting started guide. ([#1052](https://github.com/pyscript/pyscript/pull/1052))
- Added reference documentation for the `py-splashscreen` plugin ([#1138](https://github.com/pyscript/pyscript/pull/1138))
- Adds doc for installing tests ([#1156](https://github.com/pyscript/pyscript/pull/1156))
- Adds docs for custom Pyscript attributes (`py-*`) that allow you to add event listeners to an element ([#1125](https://github.com/pyscript/pyscript/pull/1125))
### Deprecations and Removals
- The py-config `runtimes` to specify an interpreter has been deprecated. The `interpreters` config should be used instead. ([#1082](https://github.com/pyscript/pyscript/pull/1082))
- The attributes `pys-onClick` and `pys-onKeyDown` have been deprecated, but the warning was only shown in the console. An alert banner will now be shown on the page if the attributes are used. They will be removed in the next release. ([#1084](https://github.com/pyscript/pyscript/pull/1084))
- The pyscript elements `py-button`, `py-inputbox`, `py-box` and `py-title` have now completed their deprecation cycle and have been removed. ([#1084](https://github.com/pyscript/pyscript/pull/1084))
- The attributes `pys-onClick` and `pys-onKeyDown` have been removed. Use `py-click` and `py-keydown` instead ([#1361](https://github.com/pyscript/pyscript/pull/1361))

130
Makefile
View File

@@ -1,122 +1,92 @@
tag := latest
git_hash ?= $(shell git log -1 --pretty=format:%h)
base_dir ?= $(shell git rev-parse --show-toplevel)
examples ?= ../$(base_dir)/examples
app_dir ?= $(shell git rev-parse --show-prefix)
CONDA_EXE := conda
CONDA_ENV ?= $(base_dir)/env
env := $(CONDA_ENV)
conda_run := $(CONDA_EXE) run -p $(env)
PYTEST_EXE := $(CONDA_ENV)/bin/pytest
MIN_NODE_VER := 14
MIN_NODE_VER := 20
MIN_NPM_VER := 6
MIN_PY3_VER := 8
NODE_VER := $(shell node -v | cut -d. -f1 | sed 's/^v\(.*\)/\1/')
NPM_VER := $(shell npm -v | cut -d. -f1)
PY3_VER := $(shell python3 -c "import sys;t='{v[1]}'.format(v=list(sys.version_info[:2]));print(t)")
PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
ifeq ($(shell uname -s), Darwin)
SED_I_ARG := -i ''
else
SED_I_ARG := -i
endif
all:
@echo "\nThere is no default Makefile target right now. Try:\n"
@echo "make setup - check your environment and install the dependencies."
@echo "make clean - clean up auto-generated assets."
@echo "make build - build PyScript."
@echo "make precommit-check - run the precommit checks (run eslint)."
@echo "make test-integration - run all integration tests sequentially."
@echo "make fmt - format the code."
@echo "make fmt-check - check the code formatting.\n"
.PHONY: check-node
check-node:
@if [ $(NODE_VER) -lt $(MIN_NODE_VER) ]; then \
echo "Build requires Node $(MIN_NODE_VER).x or higher: $(NODE_VER) detected"; \
echo "\033[0;31mBuild requires Node $(MIN_NODE_VER).x or higher: $(NODE_VER) detected.\033[0m"; \
false; \
fi
.PHONY: check-npm
check-npm:
@if [ $(NPM_VER) -lt $(MIN_NPM_VER) ]; then \
echo "Build requires Node $(MIN_NPM_VER).x or higher: $(NPM_VER) detected"; \
echo "\033[0;31mBuild requires Node $(MIN_NPM_VER).x or higher: $(NPM_VER) detected.\033[0m"; \
false; \
fi
setup: check-node check-npm
cd pyscript.core && npm install && cd ..
$(CONDA_EXE) env $(shell [ -d $(env) ] && echo update || echo create) -p $(env) --file environment.yml
$(conda_run) playwright install
$(CONDA_EXE) install -c anaconda pytest -y
.PHONY: check-python
check-python:
@if [ $(PY_OK) -eq 0 ]; then \
echo "\033[0;31mRequires Python 3.$(MIN_PY3_VER).x or higher: 3.$(PY3_VER) detected.\033[0m"; \
false; \
fi
# Check the environment, install the dependencies.
setup: check-node check-npm check-python
cd pyscript.core && npm install && cd ..
ifeq ($(VIRTUAL_ENV),)
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
false
else
python -m pip install -r requirements.txt
playwright install
endif
# Clean up generated assets.
clean:
find . -name \*.py[cod] -delete
rm -rf $(env) *.egg-info
rm -rf .pytest_cache .coverage coverage.xml
clean-all: clean
rm -rf $(env) *.egg-info
shell:
@export CONDA_ENV_PROMPT='<{name}>'
@echo 'conda activate $(env)'
dev:
cd pyscript.core && npm run dev
# Build PyScript.
build:
cd pyscript.core && npm run build
cd pyscript.core && npx playwright install && npm run build
# use the following rule to do all the checks done by precommit: in
# particular, use this if you want to run eslint.
# Run the precommit checks (run eslint).
precommit-check:
pre-commit run --all-files
examples:
mkdir -p ./examples
cp -r ../examples/* ./examples
chmod -R 755 examples
find ./examples/toga -type f -name '*.html' -exec sed $(SED_I_ARG) s+https://pyscript.net/latest/+../../build/+g {} \;
find ./examples/webgl -type f -name '*.html' -exec sed $(SED_I_ARG) s+https://pyscript.net/latest/+../../../build/+g {} \;
find ./examples -type f -name '*.html' -exec sed $(SED_I_ARG) s+https://pyscript.net/latest/+../build/+g {} \;
npm run build
rm -rf ./examples/build
mkdir -p ./examples/build
cp -R ./build/* ./examples/build
@echo "To serve examples run: $(conda_run) python -m http.server 8080 --directory examples"
# run prerequisites and serve pyscript examples at http://localhost:8000/examples/
run-examples: setup build examples
make examples
npm install
make dev
# run all integration tests *including examples* sequentially
# TODO: (fpliger) The cd pyscript.core before running the tests shouldn't be needed but for
# but for some reason it seems to bother pytest tmppaths (or test cache?). Unclear.
# Run all integration tests sequentially.
test-integration:
mkdir -p test_results
$(PYTEST_EXE) -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
pytest -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
# run all integration tests *except examples* in parallel (examples use too much memory)
# Run all integration tests in parallel.
test-integration-parallel:
mkdir -p test_results
$(PYTEST_EXE) --numprocesses auto -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
pytest --numprocesses auto -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
# run integration tests on only examples sequentially (to avoid running out of memory)
test-examples:
mkdir -p test_results
$(PYTEST_EXE) -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml -k 'zz_examples'
fmt: fmt-py fmt-ts
# Format the code.
fmt: fmt-py
@echo "Format completed"
fmt-check: fmt-ts-check fmt-py-check
# Check the code formatting.
fmt-check: fmt-py-check
@echo "Format check completed"
fmt-ts:
npm run format
fmt-ts-check:
npm run format:check
# Format Python code.
fmt-py:
$(conda_run) black --skip-string-normalization .
$(conda_run) isort --profile black .
black -l 88 --skip-string-normalization .
isort --profile black .
# Check the format of Python code.
fmt-py-check:
$(conda_run) black -l 88 --check .
black -l 88 --check .
.PHONY: $(MAKECMDGOALS)

View File

@@ -4,7 +4,7 @@
### Summary
PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [WASM](https://webassembly.org/), and modern web technologies.
PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [MicroPython](https://micropython.org/) and [WASM](https://webassembly.org/), and modern web technologies.
To get started see the [getting started tutorial](docs/tutorials/getting-started.md).
@@ -16,27 +16,55 @@ PyScript is a meta project that aims to combine multiple open technologies into
## Try PyScript
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page with:
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page:
```html
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<link
rel="stylesheet"
href="https://pyscript.net/releases/2023.11.1/core.css"
/>
<script
type="module"
src="https://pyscript.net/releases/2023.11.1/core.js"
></script>
</head>
<body>
<script type="py" terminal>
from pyscript import display
display("Hello World!") # this goes to the DOM
print("Hello terminal") # this goes to the terminal
</script>
</body>
```
You can then use PyScript components in your html page. PyScript currently implements the following elements:
You can then use PyScript components in your html page. PyScript currently offers various ways of running Python code:
- `<py-script>`: can be used to define python code that is executable within the web page. The element itself is not rendered to the page and is only used to add logic
- `<py-repl>`: creates a REPL component that is rendered to the page as a code editor and allows users to write executable code
- `<script type="py">`: can be used to define python code that is executable within the web page.
- `<script type="py" src="hello.py">`: same as above, but the python source is fetched from the given URL.
- `<script type="py" terminal>`: same as above, but also creates a terminal where to display stdout and stderr (e.g., the output of `print()`); `input()` does not work.
- `<script type="py" terminal worker>`: run Python inside a web worker: the terminal is fully functional and `input()` works.
- `<py-script>`: same as `<script type="py">`, but it is not recommended because if the code contains HTML tags, they could be parsed wrongly.
- `<script type="mpy">`: same as above but use MicroPython instead of Python.
Check out the [the examples directory](examples) folder for more examples on how to use it, all you need to do is open them in Chrome.
Check out the [official docs](https://docs.pyscript.net) for more detailed documentation.
## How to Contribute
Read the [contributing guide](CONTRIBUTING.md) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
Check out the [developing process](https://docs.pyscript.net/latest/development/developing.html) documentation for more information on how to setup your development environment.
Check out the [developing process](https://docs.pyscript.net/latest/contributing) documentation for more information on how to setup your development environment.
## Community calls and events
Every Tuesday at 15:30 UTC there is the _PyScript Community Call_ on zoom, where we can talk about PyScript development in the open. Most of the maintainers regularly participate in the call, and everybody is welcome to join.
Every other Thursday at 16:00 UTC there is the _PyScript FUN_ call: this is a call in which everybody is encouraged to show what they did with PyScript.
For more details on how to join the calls and up to date schedule, consult the official calendar:
- [Google calendar](https://calendar.google.com/calendar/u/0/embed?src=d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0@group.calendar.google.com&ctz=UTC) in UTC time;
- [iCal format](https://calendar.google.com/calendar/ical/d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0%40group.calendar.google.com/public/basic.ics).
## Resources

View File

@@ -1,26 +0,0 @@
channels:
- defaults
- conda-forge
- microsoft
dependencies:
- python=3.11.3
- pip
- pytest=7.1.2
- nodejs=16
- black
- isort
- codespell
- pre-commit
- pillow
- numpy
- markdown
- toml
- pip:
- playwright==1.33.0
- pytest-playwright==0.3.3
- pytest-xdist==3.3.0
- pexpect
# We need Pyodide and micropip so we can import them in our Python
# unit tests
- pyodide_py==0.23.2
- micropip==0.2.2

View File

@@ -19,6 +19,7 @@ module.exports = {
ecmaVersion: "latest",
sourceType: "module",
},
ignorePatterns: ["3rd-party"],
rules: {
"no-implicit-globals": ["error"],
},

31
pyscript.core/dev.cjs Normal file
View File

@@ -0,0 +1,31 @@
let queue = Promise.resolve();
const { exec } = require("node:child_process");
const build = (fileName) => {
if (fileName) console.log(fileName, "changed");
else console.log("building without optimizations");
queue = queue.then(
() =>
new Promise((resolve) => {
exec(
"npm run build:stdlib && npm run build:plugins && npm run build:core",
{ cwd: __dirname, env: { ...process.env, NO_MIN: true } },
(error) => {
if (error) console.error(error);
else console.log(fileName || "", "build completed");
resolve();
},
);
}),
);
};
const options = {
ignored: /\/(?:toml|plugins|pyscript)\.[mc]?js$/,
persistent: true,
};
require("chokidar").watch("./src", options).on("change", build);
build();

View File

@@ -159,7 +159,7 @@ The commonly shared utilities are:
* **display** in both main and worker, refers to the good old `display` utility except:
* in the *main* it automatically uses the current script `target` to display content
* in the *worker* it still needs to know *where* to display content using the `target="dom-id"` named argument, as workers don't get a default target attached
* in both main and worker, the `append=Flase` is the *default* behavior, which is a breaking change compared to classic PyScript, but because there is no `Element` with its `write(content)` utility, which would have used that `append=False` behind the scene, we've decided that `false` as append default is more desired, specially after porting most examples in *PyScript Next*, where `append=True` is the exception, not the norm.
* in both main and worker, the `append=True` is the *default* behavior, which is inherited from the classic PyScript.
#### Extra main-only features

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@pyscript/core",
"version": "0.2.7",
"version": "0.3.5",
"type": "module",
"description": "PyScript",
"module": "./index.js",
@@ -19,10 +19,18 @@
"./package.json": "./package.json"
},
"scripts": {
"server": "npx static-handler --cors --coep --coop --corp .",
"build": "node rollup/stdlib.cjs && node rollup/plugins.cjs && rm -rf dist && rollup --config rollup/core.config.js && eslint src/ && npm run ts",
"server": "npx static-handler --coi .",
"build": "npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
"build:plugins": "node rollup/plugins.cjs",
"build:stdlib": "node rollup/stdlib.cjs",
"build:3rd-party": "node rollup/3rd-party.cjs",
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/ || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"dev": "node dev.cjs",
"release": "npm run build && npm run zip",
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do echo -e \"\\033[2m$js:\\033[0m $(cat $js | brotli | wc -c) bytes\"; done",
"ts": "tsc -p ."
"ts": "tsc -p .",
"zip": "zip -r dist.zip ./dist"
},
"keywords": [
"pyscript",
@@ -33,18 +41,27 @@
"dependencies": {
"@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6",
"polyscript": "^0.4.11",
"polyscript": "^0.6.0",
"sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-terser": "^0.4.3",
"eslint": "^8.50.0",
"rollup": "^3.29.4",
"@playwright/test": "^1.40.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.3",
"@xterm/addon-fit": "^0.9.0-beta.1",
"chokidar": "^3.5.3",
"eslint": "^8.54.0",
"rollup": "^4.5.1",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0",
"static-handler": "^0.4.2",
"typescript": "^5.2.2"
"static-handler": "^0.4.3",
"typescript": "^5.3.2",
"xterm": "^5.3.0",
"xterm-readline": "^1.1.1"
},
"repository": {
"type": "git",

View File

@@ -0,0 +1,55 @@
const { copyFileSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");
const CDN = "https://cdn.jsdelivr.net/npm";
const targets = join(__dirname, "..", "src", "3rd-party");
const node_modules = join(__dirname, "..", "node_modules");
const { devDependencies } = require(join(__dirname, "..", "package.json"));
const v = (name) => devDependencies[name].replace(/[^\d.]/g, "");
// Fetch a module via jsdelivr CDN `/+esm` orchestration
// then sanitize the resulting outcome to avoid importing
// anything via `/npm/...` through Rollup
const resolve = (name) => {
const cdn = `${CDN}/${name}@${v(name)}/+esm`;
console.debug("fetching", cdn);
return fetch(cdn)
.then((b) => b.text())
.then((text) =>
text.replace(
/("|')\/npm\/(.+)?\+esm\1/g,
// normalize `/npm/module@version/+esm` as
// just `module` so that rollup can do the rest
(_, quote, module) => {
const i = module.lastIndexOf("@");
return `${quote}${module.slice(0, i)}${quote}`;
},
),
);
};
// key/value pairs as:
// "3rd-party/file-name.js"
// string as content or
// Promise<string> as resolved content
const modules = {
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
"xterm.js": resolve("xterm"),
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
(b) => b.text(),
),
"xterm-readline.js": resolve("xterm-readline"),
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
b.text(),
),
};
for (const [target, source] of Object.entries(modules)) {
if (typeof source === "string") copyFileSync(source, join(targets, target));
else {
source.then((text) => writeFileSync(join(targets, target), text));
}
}

View File

@@ -2,6 +2,7 @@
// the default exported as npm entry.
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import terser from "@rollup/plugin-terser";
import postcss from "rollup-plugin-postcss";
@@ -11,7 +12,9 @@ export default [
{
input: "./src/core.js",
plugins: plugins.concat(
process.env.NO_MIN ? [nodeResolve()] : [nodeResolve(), terser()],
process.env.NO_MIN
? [nodeResolve(), commonjs()]
: [nodeResolve(), commonjs(), terser()],
),
output: {
esModule: true,

7
pyscript.core/src/3rd-party/README.md vendored Normal file
View File

@@ -0,0 +1,7 @@
# PyScript 3rd Party
This folder contains artifacts created via [3rd-party.cjs](../../rollup/3rd-party.cjs).
As we would like to offer a way to run PyScript offline, and we already offer a `dist` folder with all the necessary scripts, we have created a foreign dependencies resolver that allow to lazy-load CDN dependencies out of the box.
Please **note** these dependencies are **not interpreters**, because interpreters have their own mechanism, folders structure, WASM files, and whatnot, to work locally, but at least XTerm or the TOML parser, among other lazy dependencies, should be available within the dist folder.

View File

@@ -89,8 +89,7 @@ for (const [TYPE] of TYPES) {
} else if (toml || type === "toml") {
try {
const { parse } = await import(
/* webpackIgnore: true */
"https://cdn.jsdelivr.net/npm/@webreflection/toml-j0.4/toml.js"
/* webpackIgnore: true */ "./3rd-party/toml.js"
);
parsed = parse(text);
} catch (e) {
@@ -114,7 +113,7 @@ for (const [TYPE] of TYPES) {
value().then(({ notify }) => notify(error.message));
}
} else if (!parsed?.plugins?.includes(`!${key}`)) {
toBeAwaited.push(value());
toBeAwaited.push(value().then(({ default: p }) => p));
}
}

View File

@@ -1,31 +1,30 @@
/*! (c) PyScript Development Team */
import stickyModule from "sticky-module";
import "@ungap/with-resolvers";
// These imports can hook more than usual and help debugging possible polyscript issues
import {
INVALID_CONTENT,
define,
Hook,
XWorker,
} from "../node_modules/polyscript/esm/index.js";
import { queryTarget } from "../node_modules/polyscript/esm/script-handler.js";
import {
assign,
dedent,
define,
defineProperty,
dispatch,
queryTarget,
unescape,
} from "../node_modules/polyscript/esm/utils.js";
import { Hook } from "../node_modules/polyscript/esm/worker/hooks.js";
whenDefined,
} from "polyscript/exports";
import "./all-done.js";
import TYPES from "./types.js";
import configs from "./config.js";
import hooks from "./hooks.js";
import sync from "./sync.js";
import stdlib from "./stdlib.js";
import bootstrapNodeAndPlugins from "./plugins-helper.js";
import { ErrorCode } from "./exceptions.js";
import { robustFetch as fetch, getText } from "./fetch.js";
const { assign, defineProperty } = Object;
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
// allows lazy element features on code evaluation
let currentElement;
@@ -33,25 +32,6 @@ let currentElement;
// generic helper to disambiguate between custom element and script
const isScript = ({ tagName }) => tagName === "SCRIPT";
// helper for all script[type="py"] out there
const before = (script) => {
defineProperty(document, "currentScript", {
configurable: true,
get: () => script,
});
};
const after = () => {
delete document.currentScript;
};
// common life-cycle handlers for any node
const bootstrapNodeAndPlugins = (wrap, element, callback, hook) => {
// make it possible to reach the current target node via Python
callback(element);
for (const fn of hooks[hook]) fn(wrap, element);
};
let shouldRegister = true;
const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
// automatically use the pyscript stderr (when/if defined)
@@ -71,25 +51,38 @@ const registerModule = ({ XWorker: $XWorker, interpreter, io }) => {
: currentElement.id;
},
});
interpreter.runPython(stdlib, { globals: interpreter.runPython("{}") });
};
const workerHooks = {
codeBeforeRunWorker: () =>
[stdlib, ...hooks.codeBeforeRunWorker].map(dedent).join("\n"),
codeBeforeRunWorkerAsync: () =>
[stdlib, ...hooks.codeBeforeRunWorkerAsync].map(dedent).join("\n"),
codeAfterRunWorker: () =>
[...hooks.codeAfterRunWorker].map(dedent).join("\n"),
codeAfterRunWorkerAsync: () =>
[...hooks.codeAfterRunWorkerAsync].map(dedent).join("\n"),
// avoid multiple initialization of the same library
const [
{
PyWorker: exportedPyWorker,
hooks: exportedHooks,
config: exportedConfig,
whenDefined: exportedWhenDefined,
},
alreadyLive,
] = stickyModule("@pyscript/core", {
PyWorker,
hooks,
config: {},
whenDefined,
});
export {
TYPES,
exportedPyWorker as PyWorker,
exportedHooks as hooks,
exportedConfig as config,
exportedWhenDefined as whenDefined,
};
const exportedConfig = {};
export { exportedConfig as config, hooks };
const hooked = new Map();
for (const [TYPE, interpreter] of TYPES) {
// avoid any dance if the module already landed
if (alreadyLive) break;
const dispatchDone = (element, isAsync, result) => {
if (isAsync) result.then(() => dispatch(element, TYPE, "done"));
else dispatch(element, TYPE, "done");
@@ -135,50 +128,11 @@ for (const [TYPE, interpreter] of TYPES) {
// possible early errors sent by polyscript
const errors = new Map();
define(TYPE, {
config,
interpreter,
env: `${TYPE}-script`,
version: config?.interpreter,
onerror(error, element) {
errors.set(element, error);
},
...workerHooks,
onWorkerReady(_, xworker) {
assign(xworker.sync, sync);
for (const callback of hooks.onWorkerReady)
callback(_, xworker);
},
onBeforeRun(wrap, element) {
currentElement = element;
bootstrapNodeAndPlugins(
wrap,
element,
before,
"onBeforeRun",
);
},
onBeforeRunAsync(wrap, element) {
currentElement = element;
bootstrapNodeAndPlugins(
wrap,
element,
before,
"onBeforeRunAsync",
);
},
onAfterRun(wrap, element) {
bootstrapNodeAndPlugins(wrap, element, after, "onAfterRun");
},
onAfterRunAsync(wrap, element) {
bootstrapNodeAndPlugins(
wrap,
element,
after,
"onAfterRunAsync",
);
},
async onInterpreterReady(wrap, element) {
// specific main and worker hooks
const hooks = {
main: {
...codeFor(main),
async onReady(wrap, element) {
if (shouldRegister) {
shouldRegister = false;
registerModule(wrap);
@@ -186,8 +140,8 @@ for (const [TYPE, interpreter] of TYPES) {
// allows plugins to do whatever they want with the element
// before regular stuff happens in here
for (const callback of hooks.onInterpreterReady)
callback(wrap, element);
for (const callback of main("onReady"))
await callback(wrap, element);
// now that all possible plugins are configured,
// bail out if polyscript encountered an error
@@ -236,6 +190,79 @@ for (const [TYPE, interpreter] of TYPES) {
}
console.debug("[pyscript/main] PyScript Ready");
},
onWorker(_, xworker) {
assign(xworker.sync, sync);
for (const callback of main("onWorker"))
callback(_, xworker);
},
onBeforeRun(wrap, element) {
currentElement = element;
bootstrapNodeAndPlugins(
main,
wrap,
element,
"onBeforeRun",
);
},
onBeforeRunAsync(wrap, element) {
currentElement = element;
return bootstrapNodeAndPlugins(
main,
wrap,
element,
"onBeforeRunAsync",
);
},
onAfterRun(wrap, element) {
bootstrapNodeAndPlugins(
main,
wrap,
element,
"onAfterRun",
);
},
onAfterRunAsync(wrap, element) {
return bootstrapNodeAndPlugins(
main,
wrap,
element,
"onAfterRunAsync",
);
},
},
worker: {
...codeFor(worker),
// these are lazy getters that returns a composition
// of the current hooks or undefined, if no hook is present
get onReady() {
return createFunction(this, "onReady", true);
},
get onBeforeRun() {
return createFunction(this, "onBeforeRun", false);
},
get onBeforeRunAsync() {
return createFunction(this, "onBeforeRunAsync", true);
},
get onAfterRun() {
return createFunction(this, "onAfterRun", false);
},
get onAfterRunAsync() {
return createFunction(this, "onAfterRunAsync", true);
},
},
};
hooked.set(TYPE, hooks);
define(TYPE, {
config,
interpreter,
hooks,
env: `${TYPE}-script`,
version: config?.interpreter,
onerror(error, element) {
errors.set(element, error);
},
});
customElements.define(
@@ -290,13 +317,14 @@ for (const [TYPE, interpreter] of TYPES) {
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Worker & {sync: ProxyHandler<object>}}
*/
export function PyWorker(file, options) {
function PyWorker(file, options) {
const hooks = hooked.get("py");
// this propagates pyscript worker hooks without needing a pyscript
// bootstrap + it passes arguments and enforces `pyodide`
// as the interpreter to use in the worker, as all hooks assume that
// and as `pyodide` is the only default interpreter that can deal with
// all the features we need to deliver pyscript out there.
const xworker = XWorker.call(new Hook(null, workerHooks), file, {
const xworker = XWorker.call(new Hook(null, hooks), file, {
type: "pyodide",
...options,
});

View File

@@ -1,3 +1,5 @@
import { assign } from "polyscript/exports";
const CLOSEBUTTON =
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor' width='12px'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>";
@@ -87,13 +89,13 @@ export function _createAlertBanner(
}
const content = messageType === "html" ? "innerHTML" : "textContent";
const banner = Object.assign(document.createElement("div"), {
const banner = assign(document.createElement("div"), {
className: `alert-banner py-${level}`,
[content]: message,
});
if (level === "warning") {
const closeButton = Object.assign(document.createElement("button"), {
const closeButton = assign(document.createElement("button"), {
id: "alert-close-button",
innerHTML: CLOSEBUTTON,
});

View File

@@ -1,5 +1,5 @@
import { FetchError, ErrorCode } from "./exceptions.js";
import { getText } from "../node_modules/polyscript/esm/fetch-utils.js";
import { getText } from "polyscript/exports";
export { getText };

View File

@@ -1,11 +1,69 @@
import { typedSet } from "type-checked-collections";
import { dedent } from "polyscript/exports";
import toJSONCallback from "to-json-callback";
import stdlib from "./stdlib.js";
export const main = (name) => hooks.main[name];
export const worker = (name) => hooks.worker[name];
const code = (hooks, branch, key, lib) => {
hooks[key] = () => {
const arr = lib ? [lib] : [];
arr.push(...branch(key));
return arr.map(dedent).join("\n");
};
};
export const codeFor = (branch) => {
const hooks = {};
code(hooks, branch, `codeBeforeRun`, stdlib);
code(hooks, branch, `codeBeforeRunAsync`, stdlib);
code(hooks, branch, `codeAfterRun`);
code(hooks, branch, `codeAfterRunAsync`);
return hooks;
};
export const createFunction = (self, name) => {
const cbs = [...worker(name)];
if (cbs.length) {
const cb = toJSONCallback(
self[`_${name}`] ||
(name.endsWith("Async")
? async (wrap, xworker, ...cbs) => {
for (const cb of cbs) await cb(wrap, xworker);
}
: (wrap, xworker, ...cbs) => {
for (const cb of cbs) cb(wrap, xworker);
}),
);
const a = cbs.map(toJSONCallback).join(", ");
return Function(`return(w,x)=>(${cb})(w,x,...[${a}])`)();
}
};
const SetFunction = typedSet({ typeof: "function" });
const SetString = typedSet({ typeof: "string" });
export default {
const inputFailure = `
import builtins
def input(prompt=""):
raise Exception("\\n ".join([
"input() doesn't work when PyScript runs in the main thread.",
"Consider using the worker attribute: https://docs.pyscript.net/2023.11.1/user-guide/workers/"
]))
builtins.input = input
del builtins
del input
`;
export const hooks = {
main: {
/** @type {Set<function>} */
onInterpreterReady: new SetFunction(),
onWorker: new SetFunction(),
/** @type {Set<function>} */
onReady: new SetFunction(),
/** @type {Set<function>} */
onBeforeRun: new SetFunction(),
/** @type {Set<function>} */
@@ -14,15 +72,33 @@ export default {
onAfterRun: new SetFunction(),
/** @type {Set<function>} */
onAfterRunAsync: new SetFunction(),
/** @type {Set<string>} */
codeBeforeRun: new SetString([inputFailure]),
/** @type {Set<string>} */
codeBeforeRunAsync: new SetString(),
/** @type {Set<string>} */
codeAfterRun: new SetString(),
/** @type {Set<string>} */
codeAfterRunAsync: new SetString(),
},
worker: {
/** @type {Set<function>} */
onWorkerReady: new SetFunction(),
onReady: new SetFunction(),
/** @type {Set<function>} */
onBeforeRun: new SetFunction(),
/** @type {Set<function>} */
onBeforeRunAsync: new SetFunction(),
/** @type {Set<function>} */
onAfterRun: new SetFunction(),
/** @type {Set<function>} */
onAfterRunAsync: new SetFunction(),
/** @type {Set<string>} */
codeBeforeRunWorker: new SetString(),
codeBeforeRun: new SetString(),
/** @type {Set<string>} */
codeBeforeRunWorkerAsync: new SetString(),
codeBeforeRunAsync: new SetString(),
/** @type {Set<string>} */
codeAfterRunWorker: new SetString(),
codeAfterRun: new SetString(),
/** @type {Set<string>} */
codeAfterRunWorkerAsync: new SetString(),
codeAfterRunAsync: new SetString(),
},
};

View File

@@ -0,0 +1,26 @@
import { defineProperty } from "polyscript/exports";
// helper for all script[type="py"] out there
const before = (script) => {
defineProperty(document, "currentScript", {
configurable: true,
get: () => script,
});
};
const after = () => {
delete document.currentScript;
};
// common life-cycle handlers for any node
export default async (main, wrap, element, hook) => {
const isAsync = hook.endsWith("Async");
const isBefore = hook.startsWith("onBefore");
// make it possible to reach the current target node via Python
// or clean up for other scripts executing around this one
(isBefore ? before : after)(element);
for (const fn of main(hook)) {
if (isAsync) await fn(wrap, element);
else fn(wrap, element);
}
};

View File

@@ -0,0 +1,27 @@
// PyScript Derepcations Plugin
import { hooks } from "../core.js";
import { notify } from "./error.js";
// react lazily on PyScript bootstrap
hooks.main.onReady.add(checkDeprecations);
hooks.main.onWorker.add(checkDeprecations);
/**
* Check that there are no scripts loading from pyscript.net/latest
*/
function checkDeprecations() {
const scripts = document.querySelectorAll("script");
for (const script of scripts) checkLoadingScriptsFromLatest(script.src);
}
/**
* Check if src being loaded from pyscript.net/latest and display a notification if true
* * @param {string} src
*/
function checkLoadingScriptsFromLatest(src) {
if (/\/pyscript\.net\/latest/.test(src)) {
notify(
"Loading scripts from latest is deprecated and will be removed soon. Please use a specific version instead.",
);
}
}

View File

@@ -1,9 +1,9 @@
// PyScript Error Plugin
import { hooks } from "../core.js";
hooks.onInterpreterReady.add(function override(pyScript) {
hooks.main.onReady.add(function override(pyScript) {
// be sure this override happens only once
hooks.onInterpreterReady.delete(override);
hooks.main.onReady.delete(override);
// trap generic `stderr` to propagate to it regardless
const { stderr } = pyScript.io;

View File

@@ -0,0 +1,150 @@
// PyScript py-terminal plugin
import { TYPES, hooks } from "../core.js";
import { notify } from "./error.js";
const SELECTOR = [...TYPES.keys()]
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
.join(",");
// show the error on main and
// stops the module from keep executing
const notifyAndThrow = (message) => {
notify(message);
throw new Error(message);
};
const pyTerminal = async () => {
const terminals = document.querySelectorAll(SELECTOR);
// no results will look further for runtime nodes
if (!terminals.length) return;
// if we arrived this far, let's drop the MutationObserver
// as we only support one terminal per page (right now).
mo.disconnect();
// we currently support only one terminal as in "classic"
if (terminals.length > 1) notifyAndThrow("You can use at most 1 terminal.");
const [element] = terminals;
// hopefully to be removed in the near future!
if (element.matches('script[type="mpy"],mpy-script'))
notifyAndThrow("Unsupported terminal.");
// import styles lazily
document.head.append(
Object.assign(document.createElement("link"), {
rel: "stylesheet",
href: new URL("./xterm.css", import.meta.url),
}),
);
// lazy load these only when a valid terminal is found
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
import(/* webpackIgnore: true */ "../3rd-party/xterm.js"),
import(/* webpackIgnore: true */ "../3rd-party/xterm-readline.js"),
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
]);
const readline = new Readline();
// common main thread initialization for both worker
// or main case, bootstrapping the terminal on its target
const init = (options) => {
let target = element;
const selector = element.getAttribute("target");
if (selector) {
target =
document.getElementById(selector) ||
document.querySelector(selector);
if (!target) throw new Error(`Unknown target ${selector}`);
} else {
target = document.createElement("py-terminal");
target.style.display = "block";
element.after(target);
}
const terminal = new Terminal({
theme: {
background: "#191A19",
foreground: "#F5F2E7",
},
...options,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.loadAddon(readline);
terminal.open(target);
fitAddon.fit();
terminal.focus();
};
// branch logic for the worker
if (element.hasAttribute("worker")) {
// when the remote thread onReady triggers:
// setup the interpreter stdout and stderr
const workerReady = ({ interpreter }, { sync }) => {
sync.pyterminal_drop_hooks();
const decoder = new TextDecoder();
let data = "";
const generic = {
isatty: true,
write(buffer) {
data = decoder.decode(buffer);
sync.pyterminal_write(data);
return buffer.length;
},
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: true,
stdin: () => sync.pyterminal_read(data),
});
};
// add a hook on the main thread to setup all sync helpers
// also bootstrapping the XTerm target on main
hooks.main.onWorker.add(function worker(_, xworker) {
hooks.main.onWorker.delete(worker);
init({
disableStdin: false,
cursorBlink: true,
cursorStyle: "block",
});
xworker.sync.pyterminal_read = readline.read.bind(readline);
xworker.sync.pyterminal_write = readline.write.bind(readline);
// allow a worker to drop main thread hooks ASAP
xworker.sync.pyterminal_drop_hooks = () => {
hooks.worker.onReady.delete(workerReady);
};
});
// setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal
hooks.worker.onReady.add(workerReady);
} else {
// in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward
hooks.main.onReady.add(function main({ io }) {
console.warn("py-terminal is read only on main thread");
hooks.main.onReady.delete(main);
init({
disableStdin: true,
cursorBlink: false,
cursorStyle: "underline",
});
io.stdout = (value) => {
readline.write(`${value}\n`);
};
io.stderr = (error) => {
readline.write(`${error.message || error}\n`);
};
});
}
};
const mo = new MutationObserver(pyTerminal);
mo.observe(document, { childList: true, subtree: true });
// try to check the current document ASAP
export default pyTerminal();

View File

@@ -10,16 +10,22 @@ import pyscript from "./stdlib/pyscript.js";
const { entries } = Object;
const python = ["from pathlib import Path as _Path"];
const python = [
"import os as _os",
"from pathlib import Path as _Path",
"_path = None",
];
const write = (base, literal) => {
for (const [key, value] of entries(literal)) {
const path = `_Path("${base}/${key}")`;
python.push(`_path = _Path("${base}/${key}")`);
if (typeof value === "string") {
const code = JSON.stringify(value);
python.push(`${path}.write_text(${code})`);
python.push(`_path.write_text(${code})`);
} else {
python.push(`${path}.mkdir(parents=True, exist_ok=True)`);
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
python.push(`if not _os.path.exists("${base}/${key}"):`);
python.push(" _path.mkdir(parents=True, exist_ok=True)");
write(`${base}/${key}`, value);
}
}
@@ -28,6 +34,8 @@ const write = (base, literal) => {
write(".", pyscript);
python.push("del _Path");
python.push("del _path");
python.push("del _os");
python.push("\n");
export default python.join("\n");

View File

@@ -6,8 +6,6 @@ from typing import Any
from pyodide.ffi import JsProxy
from pyscript import display, document, window
# from pyscript import when as _when
alert = window.alert
@@ -131,6 +129,22 @@ class Element(BaseElement):
def id(self, value):
self._js.id = value
@property
def value(self):
return self._js.value
@value.setter
def value(self, value):
# in order to avoid confusion to the user, we don't allow setting the
# value of elements that don't have a value attribute
if not hasattr(self._js, "value"):
raise AttributeError(
f"Element {self._js.tagName} has no value attribute. If you want to "
"force a value attribute, set it directly using the `_js.value = <value>` "
"javascript API attribute instead."
)
self._js.value = value
def clone(self, new_id=None):
clone = Element(self._js.cloneNode(True))
clone.id = new_id
@@ -161,9 +175,6 @@ class Element(BaseElement):
def show_me(self):
self._js.scrollIntoView()
def when(self, event, handler):
document.when(event, selector=self)(handler)
class StyleProxy(dict):
def __init__(self, element: Element) -> None:
@@ -264,6 +275,14 @@ class ElementCollection:
def html(self, value):
self._set_attribute("html", value)
@property
def value(self):
return self._get_attribute("value")
@value.setter
def value(self, value):
self._set_attribute("value", value)
@property
def children(self):
return self._elements
@@ -295,8 +314,8 @@ class PyDom(BaseElement):
self.body = Element(document.body)
self.head = Element(document.head)
def create(self, type_, parent=None, classes=None, html=None):
return super().create(type_, is_child=False)
def create(self, type_, classes=None, html=None):
return super().create(type_, is_child=False, classes=classes, html=html)
def __getitem__(self, key):
if isinstance(key, int):

View File

@@ -17,7 +17,7 @@
def on_click(event):
print(f"Hello from Python! {dt.now()}")
display(f"Hello from Python! {dt.now()}", append=False, target='eresult')
display(f"Hello from Python! {dt.now()}", append=False, target='result')
add_event_listener(element, "click", on_click)
</script>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Next Plugin Bug?</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module">
addEventListener('mpy:done', () => {
document.documentElement.classList.add('done');
});
import { hooks } from "../dist/core.js";
// Main
hooks.main.onReady.add((wrap, element) => {
console.log("main", "onReady");
if (location.search === '?debug') {
console.debug("main", "wrap", wrap);
console.debug("main", "element", element);
}
});
hooks.main.onBeforeRun.add(() => {
console.log("main", "onBeforeRun");
});
hooks.main.codeBeforeRun.add('print("main", "codeBeforeRun")');
hooks.main.codeAfterRun.add('print("main", "codeAfterRun")');
hooks.main.onAfterRun.add(() => {
console.log("main", "onAfterRun");
});
// Worker
hooks.worker.onReady.add((wrap, xworker) => {
console.log("worker", "onReady");
if (location.search === '?debug') {
console.debug("worker", "wrap", wrap);
console.debug("worker", "xworker", xworker);
}
});
hooks.worker.onBeforeRun.add(() => {
console.log("worker", "onBeforeRun");
});
hooks.worker.codeBeforeRun.add('print("worker", "codeBeforeRun")');
hooks.worker.codeAfterRun.add('print("worker", "codeAfterRun")');
hooks.worker.onAfterRun.add(() => {
console.log("worker", "onAfterRun");
});
</script>
</head>
<body>
<script type="mpy" worker>
from pyscript import document
print("actual code in worker")
document.documentElement.classList.add('worker')
</script>
<script type="mpy">
print("actual code in main")
</script>
</body>
</html>

View File

@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript Next</title>
<script>
addEventListener("py:ready", console.log);
</script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<py-script>
input("what's your name?")
</py-script>
<mpy-script>
input("what's your name?")
</mpy-script>
</body>
</html>

View File

@@ -5,7 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript Next</title>
<script>
addEventListener("mpy:ready", console.log);
addEventListener('mpy:done', () => {
document.documentElement.classList.add('done');
});
</script>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
@@ -13,11 +15,16 @@
<body>
<script type="mpy">
from pyscript import display
display("Hello", "M-PyScript Next", append=False)
display("Hello", "M-PyScript Main 1", append=False)
</script>
<mpy-script worker>
<mpy-script>
from pyscript import display
display("Hello", "M-PyScript Next Worker", append=False)
display("Hello", "M-PyScript Main 2", append=False)
</mpy-script>
<mpy-script worker>
from pyscript import display, document
display("Hello", "M-PyScript Worker", append=False)
document.documentElement.classList.add('worker')
</mpy-script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
import { test, expect } from '@playwright/test';
test('MicroPython display', async ({ page }) => {
await page.goto('http://localhost:8080/test/mpy.html');
await page.waitForSelector('html.done.worker');
const body = await page.evaluate(() => document.body.innerText);
await expect(body.trim()).toBe([
'M-PyScript Main 1',
'M-PyScript Main 2',
'M-PyScript Worker',
].join('\n'));
});
test('MicroPython hooks', async ({ page }) => {
const logs = [];
page.on('console', msg => {
const text = msg.text();
if (!text.startsWith('['))
logs.push(text);
});
await page.goto('http://localhost:8080/test/hooks.html');
await page.waitForSelector('html.done.worker');
await expect(logs.join('\n')).toBe([
'main onReady',
'main onBeforeRun',
'main codeBeforeRun',
'actual code in main',
'main codeAfterRun',
'main onAfterRun',
'worker onReady',
'worker onBeforeRun',
'worker codeBeforeRun',
'actual code in worker',
'worker codeAfterRun',
'worker onAfterRun',
].join('\n'));
});

View File

@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyTerminal</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<script type="py">
def greetings(event):
print('hello world')
</script>
<py-script worker terminal>
import sys
from pyscript import display, document
display("Hello", "PyScript Next - PyTerminal", append=False)
print("this should go to the terminal")
print("another line")
# this works as expected
print("this goes to stderr", file=sys.stderr)
document.addEventListener('click', lambda event: print(event.type));
</py-script>
<button id="my-button" py-click="greetings">Click me</button>
</body>
</html>

View File

@@ -64,8 +64,8 @@
<h2 id="multi-elem-h2" class="multi-elems">Content multi-elem-h2</h2>
<form>
<input id="test_rr_input_txt" type="text" value="Content test_rr_input_txt">
<input id="test_rr_input_btn" type="button" value="Content test_rr_input_btn">
<input id="test_rr_input_text" type="text" value="Content test_rr_input_text">
<input id="test_rr_input_button" type="button" value="Content test_rr_input_button">
<input id="test_rr_input_email" type="email" value="Content test_rr_input_email">
<input id="test_rr_input_password" type="password" value="Content test_rr_input_password">
</form>
@@ -89,7 +89,6 @@
const log = console.log.bind(console)
let testsStarted = false;
console.log = (...args) => {
log("---IN---");
let txt = args.join(" ");
let token = "<br>";
if (txt.endsWith("FAILED"))

View File

@@ -244,3 +244,51 @@ class TestCreation:
assert new_el.parent == parent_div
assert pydom[selector][0].children[0] == new_el
class TestInput:
input_ids = [
"test_rr_input_text",
"test_rr_input_button",
"test_rr_input_email",
"test_rr_input_password",
]
def test_value(self):
for id_ in self.input_ids:
expected_type = id_.split("_")[-1]
result = pydom[f"#{id_}"]
input_el = result[0]
assert input_el._js.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._js.value
# Check that we can set the value
new_value = f"New Value {expected_type}"
input_el.value = new_value
assert input_el.value == new_value
# Check that we can set the value back to the original using
# the collection
new_value = f"Content {id_}"
result.value = new_value
assert input_el.value == new_value
def test_set_value_collection(self):
for id_ in self.input_ids:
input_el = pydom[f"#{id_}"]
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
new_value = f"New Value {id_}"
input_el.value = new_value
assert input_el.value[0] == new_value == input_el[0].value
def test_element_without_value(self):
result = pydom[f"#tests-terminal"][0]
with pytest.raises(AttributeError):
result.value = "some value"
def test_element_without_collection(self):
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"

View File

@@ -1,37 +0,0 @@
<!doctype html>
<html>
<head>
<title>PyScript Next - Terminal</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@latest/css/xterm.min.css">
<script type="module">
import { Readline } from "https://cdn.jsdelivr.net/npm/xterm-readline@latest/+esm";
const rl = new Readline;
rl.setCheckHandler(text => !text.trimEnd().endsWith("&&"));
import { Terminal } from "https://cdn.jsdelivr.net/npm/xterm@latest/+esm";
const term = new Terminal({
theme: {
background: "#191A19",
foreground: "#F5F2E7",
},
cursorBlink: true,
cursorStyle: "block",
});
term.loadAddon(rl);
term.open(terminal);
term.focus();
import { PyWorker } from "../dist/core.js";
const { sync } = new PyWorker("terminal.py");
Object.assign(sync, {
readline: prompt => rl.read(prompt),
write: line => term.write(line),
});
</script>
</head>
<body>
<div id="terminal"></div>
</body>
</html>

View File

@@ -1,14 +0,0 @@
###### magic monkey patching ######
import builtins
import sys
from pyodide.code import eval_code
from pyscript import sync
sys.stdout = sync
builtins.input = sync.readline
####### main code ######
import code
code.interact()

View File

@@ -8,8 +8,10 @@
<!-- the PyWorker approach -->
<script type="module">
import { PyWorker } from '../dist/core.js';
import { PyWorker, whenDefined } from '../dist/core.js';
whenDefined('py').then(() => {
PyWorker('./worker.py', {config: {fetch: [{files: ['./a.py']}]}});
});
// the type is overwritten as "pyodide" in PyScript as the module
// lives in that env too
</script>

View File

@@ -118,6 +118,20 @@ def only_main(fn):
return decorated
def only_worker(fn):
"""
Decorator to mark a test which make sense only in the worker thread
"""
@functools.wraps(fn)
def decorated(self, *args):
if self.execution_thread != "worker":
return
return fn(self, *args)
return decorated
def filter_inner_text(text, exclude=None):
return "\n".join(filter_page_content(text.splitlines(), exclude=exclude))
@@ -531,7 +545,9 @@ class PyScriptTest:
- wait until pyscript has been fully loaded
"""
doc = self._pyscript_format(
snippet, execution_thread=self.execution_thread, extra_head=extra_head
snippet,
execution_thread=self.execution_thread,
extra_head=extra_head,
)
if not wait_for_pyscript and timeout is not None:
raise ValueError("Cannot set a timeout if wait_for_pyscript=False")
@@ -652,7 +668,7 @@ TEST_ITERATIONS = math.ceil(
) # 120 iters of 1/4 second
def wait_for_render(page, selector, pattern, timeout_seconds: int | None = None):
def wait_for_render(page, selector, pattern, timeout_seconds=None):
"""
Assert that rendering inserts data into the page as expected: search the
DOM from within the timing loop for a string that is not present in the

View File

@@ -80,7 +80,7 @@ class TestBasic(PyScriptTest):
'"Cross-Origin-Opener-Policy":"same-origin"}. '
"The problem may be that one or both of these are missing."
)
alert_banner = self.page.wait_for_selector(".alert-banner")
alert_banner = self.page.wait_for_selector(".py-error")
assert expected_alert_banner_msg in alert_banner.inner_text()
def test_print(self):
@@ -93,6 +93,19 @@ class TestBasic(PyScriptTest):
)
assert self.console.log.lines[-1] == "hello pyscript"
@only_main
def test_input_exception(self):
self.pyscript_run(
"""
<script type="py">
input("what's your name?")
</script>
"""
)
self.check_py_errors(
"Exception: input() doesn't work when PyScript runs in the main thread."
)
@skip_worker("NEXT: exceptions should be displayed in the DOM")
def test_python_exception(self):
self.pyscript_run(

View File

@@ -3,15 +3,62 @@ import time
import pytest
from playwright.sync_api import expect
from .support import PyScriptTest, skip_worker
pytest.skip(
reason="FIX LATER: pyscript NEXT doesn't support the Terminal yet",
allow_module_level=True,
)
from .support import PageErrors, PyScriptTest, only_worker, skip_worker
class TestPyTerminal(PyScriptTest):
def test_multiple_terminals(self):
"""
Multiple terminals are not currently supported
"""
self.pyscript_run(
"""
<script type="py" terminal></script>
<script type="py" terminal></script>
""",
wait_for_pyscript=False,
check_js_errors=False,
)
assert self.assert_banner_message("You can use at most 1 terminal")
with pytest.raises(PageErrors, match="You can use at most 1 terminal"):
self.check_js_errors()
# TODO: interactive shell still unclear
# @only_worker
# def test_py_terminal_input(self):
# """
# Only worker py-terminal accepts an input
# """
# self.pyscript_run(
# """
# <script type="py" terminal></script>
# """,
# wait_for_pyscript=False,
# )
# self.page.get_by_text(">>> ", exact=True).wait_for()
# self.page.keyboard.type("'the answer is ' + str(6 * 7)")
# self.page.keyboard.press("Enter")
# self.page.get_by_text("the answer is 42").wait_for()
@only_worker
def test_py_terminal_os_write(self):
"""
An `os.write("text")` should land in the terminal
"""
self.pyscript_run(
"""
<script type="py" terminal>
import os
os.write(1, str.encode("hello\\n"))
os.write(2, str.encode("world\\n"))
</script>
""",
wait_for_pyscript=False,
)
self.page.get_by_text("hello\n").wait_for()
self.page.get_by_text("world\n").wait_for()
def test_py_terminal(self):
"""
1. <py-terminal> should redirect stdout and stderr to the DOM
@@ -20,138 +67,44 @@ class TestPyTerminal(PyScriptTest):
"""
self.pyscript_run(
"""
<py-terminal></py-terminal>
<script type="py">
<script type="py" terminal>
import sys
print('hello world')
print('this goes to stderr', file=sys.stderr)
print('this goes to stdout')
</script>
"""
""",
wait_for_pyscript=False,
)
self.page.get_by_text("hello world").wait_for()
term = self.page.locator("py-terminal")
term_lines = term.inner_text().splitlines()
assert term_lines == [
"hello world",
"this goes to stderr",
"this goes to stdout",
]
assert self.console.log.lines[-3:] == [
assert term_lines[0:3] == [
"hello world",
"this goes to stderr",
"this goes to stdout",
]
@skip_worker("FIXME: js.document")
def test_two_terminals(self):
"""
Multiple <py-terminal>s can cohexist.
A <py-terminal> receives only output from the moment it is added to
the DOM.
"""
@skip_worker(
"Workers don't have events + two different workers don't share the same I/O"
)
def test_button_action(self):
self.pyscript_run(
"""
<py-terminal id="term1"></py-terminal>
<script type="py">
import js
print('one')
term2 = js.document.createElement('py-terminal')
term2.id = 'term2'
js.document.body.append(term2)
print('two')
print('three')
</script>
"""
)
term1 = self.page.locator("#term1")
term2 = self.page.locator("#term2")
term1_lines = term1.inner_text().splitlines()
term2_lines = term2.inner_text().splitlines()
assert term1_lines == ["one", "two", "three"]
assert term2_lines == ["two", "three"]
def test_auto_attribute(self):
self.pyscript_run(
"""
<py-terminal auto></py-terminal>
<button id="my-button" py-click="print('hello world')">Click me</button>
"""
)
term = self.page.locator("py-terminal")
expect(term).to_be_hidden()
self.page.locator("button").click()
expect(term).to_be_visible()
assert term.inner_text() == "hello world\n"
def test_config_auto(self):
"""
config.terminal == "auto" is the default: a <py-terminal auto> is
automatically added to the page
"""
self.pyscript_run(
"""
<button id="my-button" py-click="print('hello world')">Click me</button>
"""
)
term = self.page.locator("py-terminal")
expect(term).to_be_hidden()
assert "No <py-terminal> found, adding one" in self.console.info.text
#
self.page.locator("button").click()
expect(term).to_be_visible()
assert term.inner_text() == "hello world\n"
def test_config_true(self):
"""
If we set config.terminal == true, a <py-terminal> is automatically added
"""
self.pyscript_run(
"""
<py-config>
terminal = true
</py-config>
<script type="py">
def greetings(event):
print('hello world')
</script>
"""
)
term = self.page.locator("py-terminal")
expect(term).to_be_visible()
assert term.inner_text() == "hello world\n"
<script type="py" terminal></script>
def test_config_false(self):
"""
If we set config.terminal == false, no <py-terminal> is added
"""
self.pyscript_run(
"""
<py-config>
terminal = false
</py-config>
"""
)
term = self.page.locator("py-terminal")
assert term.count() == 0
def test_config_docked(self):
"""
config.docked == "docked" is also the default: a <py-terminal auto docked> is
automatically added to the page
"""
self.pyscript_run(
"""
<button id="my-button" py-click="print('hello world')">Click me</button>
<button id="my-button" py-click="greetings">Click me</button>
"""
)
term = self.page.locator("py-terminal")
self.page.locator("button").click()
expect(term).to_be_visible()
assert term.get_attribute("docked") == ""
last_line = self.page.get_by_text("hello world")
last_line.wait_for()
assert term.inner_text().rstrip() == "hello world"
def test_xterm_function(self):
"""Test a few basic behaviors of the xtermjs terminal.
@@ -164,17 +117,15 @@ class TestPyTerminal(PyScriptTest):
"""
self.pyscript_run(
"""
<py-config>
xterm = true
</py-config>
<script type="py">
<script type="py" terminal>
print("\x1b[33mYellow\x1b[0m")
print("\x1b[4mUnderline\x1b[24m")
print("\x1b[1mBold\x1b[22m")
print("\x1b[3mItalic\x1b[23m")
print("done")
</script>
"""
""",
wait_for_pyscript=False,
)
# Wait for "done" to actually appear in the xterm; may be delayed,
@@ -234,37 +185,3 @@ class TestPyTerminal(PyScriptTest):
"(element) => getComputedStyle(element).getPropertyValue('font-style')"
)
assert font_style == "italic"
def test_xterm_multiple(self):
"""Test whether multiple x-terms on the page all function"""
self.pyscript_run(
"""
<py-config>
xterm = true
</py-config>
<script type="py">
print("\x1b[33mYellow\x1b[0m")
print("done")
</script>
<py-terminal id="a"></py-terminal>
<py-terminal id="b" data-testid="b"></py-terminal>
"""
)
# Wait for "done" to actually appear in the xterm; may be delayed,
# since xtermjs processes its input buffer in chunks
last_line = self.page.get_by_test_id("b").get_by_text("done")
last_line.wait_for()
# Yes, this is not ideal. See note in `test_xterm_function`
time.sleep(1)
rows = self.page.locator("#a .xterm-rows")
# First line should be yellow
first_line = rows.locator("div").nth(0)
first_char = first_line.locator("span").nth(0)
color = first_char.evaluate(
"(element) => getComputedStyle(element).getPropertyValue('color')"
)
assert color == "rgb(196, 160, 0)"

View File

@@ -1,13 +1,35 @@
import pytest
from .support import PyScriptTest
pytest.skip(reason="NEXT: Restore the banner", allow_module_level=True)
from .support import PyScriptTest, skip_worker
class TestWarningsAndBanners(PyScriptTest):
# Test the behavior of generated warning banners
def test_deprecate_loading_scripts_from_latest(self):
# Use a script tag with an invalid output attribute to generate a warning, but only one
self.pyscript_run(
"""
<script type="py">
print("whatever..")
</script>
""",
extra_head='<script type="ignore-me" src="https://pyscript.net/latest/any-path-triggers-the-warning-anyway.js"></script>',
)
# wait for the banner to appear (we could have a page.locater call but for some reason
# the worker takes to long to render on CI, since it's a test we can afford 2 calls)
loc = self.page.wait_for_selector(".py-error")
assert (
loc.inner_text()
== "Loading scripts from latest is deprecated and will be removed soon. Please use a specific version instead."
)
# Only one banner should appear
loc = self.page.locator(".py-error")
assert loc.count() == 1
@pytest.mark.skip("NEXT: To check if behaviour is consistent with classic")
def test_create_singular_warning(self):
# Use a script tag with an invalid output attribute to generate a warning, but only one
self.pyscript_run(

View File

@@ -1,8 +1,8 @@
{
"compilerOptions": {
"module": "ES2022",
"target": "ES2022",
"moduleResolution": "Classic",
"module": "NodeNext",
"target": "esnext",
"moduleResolution": "nodenext",
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,

12
pyscript.core/types/3rd-party/toml.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/*! (c) Jak Wings - MIT */ declare class e extends SyntaxError {
constructor(r: any, { offset: t, line: e, column: n }: {
offset: any;
line: any;
column: any;
});
offset: any;
line: any;
column: any;
}
declare function n(n: any): any;
export { e as SyntaxError, n as parse };

View File

@@ -0,0 +1,138 @@
declare var b: any;
declare var I: boolean;
declare namespace r {
export let __esModule: boolean;
export { Readline };
}
declare class Readline {
highlighter: {
highlight(t: any, e: any): any;
highlightPrompt(t: any): any;
highlightChar(t: any, e: any): boolean;
};
history: {
entries: any[];
cursor: number;
maxEntries: any;
saveToLocalStorage(): void;
restoreFromLocalStorage(): void;
append(t: any): void;
resetCursor(): void;
next(): any;
prev(): any;
};
disposables: any[];
watermark: number;
highWatermark: number;
lowWatermark: number;
highWater: boolean;
state: {
line: {
buf: string;
pos: number;
buffer(): string;
pos_buffer(): string;
length(): number;
char_length(): number;
update(t: any, e: any): void;
insert(t: any): boolean;
moveBack(t: any): boolean;
moveForward(t: any): boolean;
moveHome(): boolean;
moveEnd(): boolean;
startOfLine(): number;
endOfLine(): number;
moveLineUp(t: any): boolean;
moveLineDown(t: any): boolean;
set_pos(t: any): void;
prevPos(t: any): number;
nextPos(t: any): number;
backspace(t: any): boolean;
delete(t: any): boolean;
deleteEndOfLine(): boolean;
};
highlighting: boolean;
prompt: any;
tty: any;
highlighter: any;
history: any;
promptSize: any;
layout: p;
buffer(): string;
shouldHighlight(): boolean;
clearScreen(): void;
editInsert(t: any): void;
update(t: any): void;
editBackspace(t: any): void;
editDelete(t: any): void;
editDeleteEndOfLine(): void;
refresh(): void;
moveCursorBack(t: any): void;
moveCursorForward(t: any): void;
moveCursorUp(t: any): void;
moveCursorDown(t: any): void;
moveCursorHome(): void;
moveCursorEnd(): void;
moveCursorToEnd(): void;
previousHistory(): void;
nextHistory(): void;
moveCursor(): void;
};
checkHandler: () => boolean;
ctrlCHandler: () => void;
pauseHandler: (t: any) => void;
activate(t: any): void;
term: any;
dispose(): void;
appendHistory(t: any): void;
setHighlighter(t: any): void;
setCheckHandler(t: any): void;
setCtrlCHandler(t: any): void;
setPauseHandler(t: any): void;
writeReady(): boolean;
write(t: any): void;
print(t: any): void;
println(t: any): void;
output(): this;
tty(): {
tabWidth: any;
col: any;
row: any;
out: any;
write(t: any): any;
print(t: any): any;
println(t: any): any;
clearScreen(): void;
calculatePosition(t: any, e: any): any;
computeLayout(t: any, e: any): {
promptSize: any;
cursor: any;
end: any;
};
refreshLine(t: any, e: any, s: any, i: any, r: any): void;
clearOldRows(t: any): void;
moveCursor(t: any, e: any): void;
};
read(t: any): Promise<any>;
activeRead: {
prompt: any;
resolve: (value: any) => void;
reject: (reason?: any) => void;
};
handleKeyEvent(t: any): boolean;
readData(t: any): void;
readPaste(t: any): void;
readKey(t: any): void;
}
declare class p {
constructor(t: any);
promptSize: any;
cursor: c;
end: c;
}
declare class c {
constructor(t: any, e: any);
row: any;
col: any;
}
export { b as Readline, I as __esModule, r as default };

View File

@@ -0,0 +1,4 @@
declare var i: any;
declare var s: any;
declare var t: {};
export { i as Terminal, s as __esModule, t as default };

View File

@@ -0,0 +1,4 @@
declare var i: any;
declare var o: any;
declare var s: {};
export { i as FitAddon, o as __esModule, s as default };

View File

@@ -1,16 +1,42 @@
import TYPES from "./types.js";
/**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker.
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Worker & {sync: ProxyHandler<object>}}
*/
export function PyWorker(file: string, options?: {
declare function exportedPyWorker(file: string, options?: {
config?: string | object;
async?: boolean;
}): Worker & {
sync: ProxyHandler<object>;
};
import sync from "./sync.js";
declare const exportedHooks: {
main: {
onWorker: Set<Function>;
onReady: Set<Function>;
onBeforeRun: Set<Function>;
onBeforeRunAsync: Set<Function>;
onAfterRun: Set<Function>;
onAfterRunAsync: Set<Function>;
codeBeforeRun: Set<string>;
codeBeforeRunAsync: Set<string>;
codeAfterRun: Set<string>;
codeAfterRunAsync: Set<string>;
};
worker: {
onReady: Set<Function>;
onBeforeRun: Set<Function>;
onBeforeRunAsync: Set<Function>;
onAfterRun: Set<Function>;
onAfterRunAsync: Set<Function>;
codeBeforeRun: Set<string>;
codeBeforeRunAsync: Set<string>;
codeAfterRun: Set<string>;
codeAfterRunAsync: Set<string>;
};
};
declare const exportedConfig: {};
import hooks from "./hooks.js";
export { exportedConfig as config, hooks };
declare const exportedWhenDefined: (type: string) => Promise<any>;
import sync from "./sync.js";
export { TYPES, exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };

View File

@@ -9,3 +9,4 @@
*/
export function robustFetch(url: string, options?: Request): Promise<Response>;
export { getText };
import { getText } from "polyscript/exports";

View File

@@ -1,13 +1,38 @@
declare namespace _default {
let onInterpreterReady: Set<Function>;
export function main(name: any): any;
export function worker(name: any): any;
export function codeFor(branch: any): {};
export function createFunction(self: any, name: any): any;
export namespace hooks {
namespace main {
let onWorker: Set<Function>;
let onReady: Set<Function>;
let onBeforeRun: Set<Function>;
let onBeforeRunAsync: Set<Function>;
let onAfterRun: Set<Function>;
let onAfterRunAsync: Set<Function>;
let onWorkerReady: Set<Function>;
let codeBeforeRunWorker: Set<string>;
let codeBeforeRunWorkerAsync: Set<string>;
let codeAfterRunWorker: Set<string>;
let codeAfterRunWorkerAsync: Set<string>;
let codeBeforeRun: Set<string>;
let codeBeforeRunAsync: Set<string>;
let codeAfterRun: Set<string>;
let codeAfterRunAsync: Set<string>;
}
namespace worker {
let onReady_1: Set<Function>;
export { onReady_1 as onReady };
let onBeforeRun_1: Set<Function>;
export { onBeforeRun_1 as onBeforeRun };
let onBeforeRunAsync_1: Set<Function>;
export { onBeforeRunAsync_1 as onBeforeRunAsync };
let onAfterRun_1: Set<Function>;
export { onAfterRun_1 as onAfterRun };
let onAfterRunAsync_1: Set<Function>;
export { onAfterRunAsync_1 as onAfterRunAsync };
let codeBeforeRun_1: Set<string>;
export { codeBeforeRun_1 as codeBeforeRun };
let codeBeforeRunAsync_1: Set<string>;
export { codeBeforeRunAsync_1 as codeBeforeRunAsync };
let codeAfterRun_1: Set<string>;
export { codeAfterRun_1 as codeAfterRun };
let codeAfterRunAsync_1: Set<string>;
export { codeAfterRunAsync_1 as codeAfterRunAsync };
}
}
export default _default;

View File

@@ -0,0 +1,2 @@
declare function _default(main: any, wrap: any, element: any, hook: any): Promise<void>;
export default _default;

View File

@@ -1,4 +1,6 @@
declare namespace _default {
function error(): Promise<typeof import("./plugins/error.js")>;
}
declare const _default: {
"deprecations-manager": () => Promise<typeof import("./plugins/deprecations-manager.js")>;
error: () => Promise<typeof import("./plugins/error.js")>;
"py-terminal": () => Promise<typeof import("./plugins/py-terminal.js")>;
};
export default _default;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,2 @@
declare const _default: Promise<void>;
export default _default;

12
pyscript.core/types/toml.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/*! (c) Jak Wings - MIT */ declare class e extends SyntaxError {
constructor(r: any, { offset: t, line: e, column: n }: {
offset: any;
line: any;
column: any;
});
offset: any;
line: any;
column: any;
}
declare function n(n: any): any;
export { e as SyntaxError, n as parse };

13
requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
black
isort
pytest==7.1.2
pre-commit
playwright==1.33.0
pytest-playwright==0.3.3
pytest-xdist==3.3.0
pexpect
pyodide_py==0.24.1
micropip
toml
numpy
pillow