2021-03-14 10:15:53 +00:00
|
|
|
(type-translations)=
|
|
|
|
# Type translations
|
|
|
|
In order to communicate between Python and Javascript, we "translate" objects
|
|
|
|
between the two languages. Depending on the type of the object we either
|
|
|
|
translate the object by implicitly converting it or by proxying it. By
|
|
|
|
"converting" an object we mean producing a new object in the target language
|
|
|
|
which is the equivalent of the object from the source language, for example
|
|
|
|
converting a Python string to the equivalent a Javascript string. By "proxying"
|
|
|
|
an object we mean producing a special object in the target language that
|
|
|
|
forwards requests to the source language. When we proxy a Javascript object into
|
|
|
|
Python, the result is a `JsProxy` object. When we proxy a Python object into
|
|
|
|
Javascript, the result is a `PyProxy` object. A proxied object can be explicitly
|
|
|
|
converted using the explicit conversion methods `JsProxy.to_py` and
|
|
|
|
`PyProxy.toJs`.
|
|
|
|
|
|
|
|
Python to Javascript translations occur:
|
|
|
|
|
|
|
|
- when returning the final expression from a {any}`pyodide.runPython` call,
|
2021-03-24 11:05:00 +00:00
|
|
|
- when using `pyodide.globals.get('key')`,
|
2021-03-14 10:15:53 +00:00
|
|
|
- when passing arguments to a Javascript function called from Python,
|
|
|
|
- when returning the results of a Python function called from Javascript,
|
|
|
|
- when accessing an attribute of a `PyProxy`
|
|
|
|
|
|
|
|
Javascript to Python translations occur:
|
2018-06-21 15:19:34 +00:00
|
|
|
|
|
|
|
- when using the `from js import ...` syntax
|
2021-03-14 10:15:53 +00:00
|
|
|
- passing arguments to a Python function called from Javascript
|
|
|
|
- returning the result of a Javascript function called from Python
|
|
|
|
- when accessing an attribute of a `JsProxy`
|
|
|
|
|
|
|
|
`````{admonition} Memory Leaks and Python to Javascript translations
|
|
|
|
:class: warning
|
|
|
|
|
|
|
|
Any time a Python to Javascript translation occurs, it may create a `PyProxy`.
|
|
|
|
To avoid memory leaks, you must store the result and destroy it when you are
|
|
|
|
done with it. Unfortunately, we currently provide no convenient way to do this,
|
|
|
|
particularly when calling Javascript functions from Python.
|
|
|
|
`````
|
|
|
|
|
2021-03-24 11:05:00 +00:00
|
|
|
## Round trip conversions
|
2021-03-14 10:15:53 +00:00
|
|
|
Translating an object from Python to Javascript and then back to
|
|
|
|
Python is guaranteed to give an object that is equal to the original object
|
|
|
|
(with the exception of `nan` because `nan != nan`). Furthermore, if the object
|
|
|
|
is proxied into Javascript, then translation back unwraps the proxy, and the
|
|
|
|
result of the round trip conversion `is` the original object (in the sense that
|
|
|
|
they live at the same memory address).
|
|
|
|
|
|
|
|
Translating an object from Javascript to Python and then back to
|
|
|
|
Javascript gives an object that is `===` to the original object (with the
|
|
|
|
exception of `NaN` because `NaN !== NaN`, and of `null` which after a round trip
|
|
|
|
is converted to `undefined`). Furthermore, if the object is proxied into Python,
|
|
|
|
then translation back unwraps the proxy, and the result of the round trip
|
|
|
|
conversion is the original object (in the sense that they live at the same
|
|
|
|
memory address).
|
|
|
|
|
|
|
|
## Implicit conversions
|
|
|
|
|
|
|
|
We only implicitly convert immutable types. This is to ensure that a mutable
|
|
|
|
type in Python can be modified in Javascript and vice-versa. Python has
|
|
|
|
immutable types such as `tuple` and `bytes` that have no equivalent in
|
|
|
|
Javascript. In order to ensure that round trip translations yield an object of
|
|
|
|
the same type as the original object, we proxy `tuple` and `bytes` objects.
|
|
|
|
Proxying tuples also has the benefit of ensuring that implicit conversions take
|
|
|
|
a constant amount of time.
|
|
|
|
|
|
|
|
### Python to Javascript
|
|
|
|
The following immutable types are implicitly converted from Javascript to
|
|
|
|
Python:
|
2018-06-21 15:19:34 +00:00
|
|
|
|
2018-06-22 15:49:32 +00:00
|
|
|
| Python | Javascript |
|
|
|
|
|-----------------|---------------------|
|
2021-03-14 10:15:53 +00:00
|
|
|
| `int` | `Number` |
|
|
|
|
| `float` | `Number` |
|
2018-06-22 15:49:32 +00:00
|
|
|
| `str` | `String` |
|
2021-03-14 10:15:53 +00:00
|
|
|
| `bool` | `Boolean` |
|
|
|
|
| `None` | `undefined` |
|
2018-06-29 20:34:18 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
### Javascript to Python
|
|
|
|
The following immutable types are implicitly converted from Python to
|
|
|
|
Javascript:
|
2018-06-29 20:34:18 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
| Javascript | Python |
|
|
|
|
|-----------------|---------------------------------|
|
|
|
|
| `Number` | `int` or `float` as appropriate |
|
|
|
|
| `String` | `str` |
|
|
|
|
| `Boolean` | `bool` |
|
|
|
|
| `undefined` | `None` |
|
|
|
|
| `null` | `None` |
|
2018-06-29 20:34:18 +00:00
|
|
|
|
2018-06-21 15:19:34 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
## Proxying
|
2018-06-21 15:19:34 +00:00
|
|
|
|
|
|
|
Any of the types not listed above are shared between languages using proxies
|
2021-03-14 10:15:53 +00:00
|
|
|
that allow methods and some operations to be called on the object from the other
|
2018-06-21 15:19:34 +00:00
|
|
|
language.
|
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
### Proxying from Javascript into Python
|
|
|
|
|
|
|
|
When most Javascript objects are translated into Python a `JsProxy` is returned.
|
|
|
|
The following operations are currently supported on a `JsProxy`. (More should be
|
|
|
|
possible in the future -- work is ongoing to make this more complete):
|
|
|
|
|
|
|
|
| Python | Javascript |
|
|
|
|
|---------------------------|------------------------|
|
|
|
|
| `str(proxy)` | `x.toString()` |
|
|
|
|
| `proxy.foo` | `x.foo` |
|
|
|
|
| `proxy.foo = bar` | `x.foo = bar` |
|
|
|
|
| `del proxy.foo` | `delete x.foo` |
|
|
|
|
| `hasattr(proxy, "foo")` | `"foo" in x` |
|
|
|
|
| `proxy(...)` | `x(...)` |
|
|
|
|
| `proxy.foo(...)` | `x.foo(...)` |
|
|
|
|
| `proxy.new(...)` | `new X(...)` |
|
|
|
|
| `len(proxy)` | `x.length` or `x.size` |
|
|
|
|
| `foo in proxy` | `x.has(foo)` |
|
|
|
|
| `proxy[foo]` | `x.get(foo)` |
|
|
|
|
| `proxy[foo] = bar` | `x.set(foo, bar)` |
|
|
|
|
| `del proxy[foo]` | `x.delete(foo)` |
|
|
|
|
| `proxy1 == proxy2` | `x === y` |
|
|
|
|
| `proxy.typeof` | `typeof x` |
|
|
|
|
| `iter(proxy)` | `x[Symbol.iterator]()` |
|
|
|
|
| `next(proxy)` | `x.next()` |
|
|
|
|
| `await proxy` | `await x` |
|
|
|
|
| `proxy.object_entries()` | `Object.entries(x)` |
|
|
|
|
|
|
|
|
Some other code snippets:
|
|
|
|
```py
|
|
|
|
for v in proxy:
|
|
|
|
# do something
|
|
|
|
```
|
|
|
|
is equivalent to:
|
|
|
|
```js
|
|
|
|
for(let v of x){
|
|
|
|
// do something
|
|
|
|
}
|
|
|
|
```
|
|
|
|
The `dir` method has been overloaded to return all keys on the prototype chain
|
|
|
|
of `x`, so `dir(x)` roughly translates to:
|
|
|
|
```js
|
|
|
|
function dir(x){
|
|
|
|
let result = [];
|
|
|
|
do {
|
|
|
|
result.push(...Object.getOwnPropertyNames(x));
|
|
|
|
} while (x = Object.getPrototypeOf(x));
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
```
|
2020-10-30 20:09:25 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
As a special case, Javascript `Array`, `HTMLCollection`, and `NodeList` are
|
|
|
|
container types, but instead of using `array.get(7)` to get the 7th element,
|
|
|
|
Javascript uses `array["7"]`. For these cases, we translate:
|
|
|
|
|
|
|
|
| Python | Javascript |
|
|
|
|
|---------------------------|------------------------|
|
|
|
|
| `proxy[idx]` | `x.toString()` |
|
|
|
|
| `proxy[idx] = val` | `x.foo` |
|
|
|
|
| `idx in proxy` | `idx in array` |
|
|
|
|
| `del proxy[idx]` | `proxy.splice(idx)` |
|
|
|
|
|
|
|
|
|
2021-03-25 16:26:07 +00:00
|
|
|
(type-translations-pyproxy)=
|
2021-03-14 10:15:53 +00:00
|
|
|
### Proxying from Python into Javascript
|
|
|
|
|
|
|
|
When most Python objects are translated to Javascript a `PyProxy` is produced.
|
|
|
|
Fewer operations can be overloaded in Javascript than in Python so some
|
|
|
|
operations are more cumbersome on a `PyProxy` than on a `JsProxy`. The following
|
|
|
|
operations are currently supported:
|
|
|
|
|
|
|
|
| Javascript | Python |
|
|
|
|
|---------------------------------------|--------------------------|
|
|
|
|
| `foo in proxy` | `hasattr(x, 'foo')` |
|
|
|
|
| `proxy.foo` | `x.foo` |
|
|
|
|
| `proxy.foo = bar` | `x.foo = bar` |
|
|
|
|
| `delete proxy.foo` | `del x.foo` |
|
|
|
|
| `Object.getOwnPropertyNames(proxy)` | `dir(x)` |
|
|
|
|
| `proxy(...)` | `x(...)` |
|
|
|
|
| `proxy.foo(...)` | `x.foo(...)` |
|
|
|
|
| `proxy.length` or `x.size` | `len(x)` |
|
|
|
|
| `proxy.has(foo)` | `foo in x` |
|
|
|
|
| `proxy.get(foo)` | `x[foo]` |
|
|
|
|
| `proxy.set(foo, bar)` | `x[foo] = bar` |
|
|
|
|
| `proxy.delete(foo)` | `del x[foo]` |
|
|
|
|
| `x.type` | `type(x)` |
|
|
|
|
| `x[Symbol.iterator]()` | `iter(x)` |
|
|
|
|
| `x.next()` | `next(x)` |
|
|
|
|
| `await x` | `await x` |
|
|
|
|
| `Object.entries(x)` | `repr(x)` |
|
|
|
|
|
|
|
|
`````{admonition} Memory Leaks and PyProxy
|
|
|
|
:class: warning
|
|
|
|
When proxying a Python object into Javascript, there is no way for Javascript to
|
|
|
|
automatically garbage collect the Proxy. The `PyProxy` must be manually
|
|
|
|
destroyed when passed to Javascript, or the proxied Python object will leak. To
|
|
|
|
do this, call `PyProxy.destroy()` on the `PyProxy`, after which Javascript will
|
|
|
|
no longer have access to the Python object. If no references to the Python
|
|
|
|
object exist in Python either, then the Python garbage collector can eventually
|
|
|
|
collect it.
|
2018-06-24 16:29:46 +00:00
|
|
|
|
|
|
|
```javascript
|
2021-03-24 11:05:00 +00:00
|
|
|
let foo = pyodide.globals.get('foo');
|
2021-03-14 10:15:53 +00:00
|
|
|
foo();
|
2018-06-24 16:29:46 +00:00
|
|
|
foo.destroy();
|
2021-03-14 10:15:53 +00:00
|
|
|
foo(); // throws Error: Object has already been destroyed
|
|
|
|
```
|
|
|
|
`````
|
|
|
|
|
|
|
|
`````{admonition} Memory Leaks and PyProxy method calls
|
|
|
|
:class: warning
|
|
|
|
|
|
|
|
Every time you access a Python method on a `PyProxy`, it creates a new temporary
|
|
|
|
`PyProxy` of a Python bound method. If you do not capture this temporary and
|
2021-03-24 11:05:00 +00:00
|
|
|
destroy it, you will leak the Python object.
|
2021-03-14 10:15:53 +00:00
|
|
|
`````
|
|
|
|
Here's an example:
|
|
|
|
|
|
|
|
```pyodide
|
|
|
|
pyodide.runPython(`
|
|
|
|
class Test(dict):
|
|
|
|
def __del__(self):
|
|
|
|
print("destructed!")
|
|
|
|
d = Test(a=2, b=3)
|
|
|
|
import sys
|
|
|
|
print(sys.getrefcount(d)) # prints 2
|
|
|
|
`);
|
2021-03-24 11:05:00 +00:00
|
|
|
let d = pyodide.globals.get("d");
|
2021-03-14 10:15:53 +00:00
|
|
|
// Leak three temporary bound "get" methods!
|
|
|
|
let l = [d.get("a", 0), d.get("b", 0), d.get("c", 0)];
|
|
|
|
d.destroy(); // Try to free dict
|
|
|
|
// l is [2, 3, 0].
|
|
|
|
pyodide.runPython(`
|
|
|
|
print(sys.getrefcount(d)) # prints 5 = original 2 + leaked 3
|
|
|
|
del d # Destructor isn't run because of leaks
|
|
|
|
`);
|
|
|
|
```
|
|
|
|
Here is how we can do this without leaking:
|
|
|
|
```pyodide
|
2021-03-24 11:05:00 +00:00
|
|
|
let d = pyodide.globals.get("d");
|
2021-03-14 10:15:53 +00:00
|
|
|
let d_get = d.get; // this time avoid the leak
|
|
|
|
let l = [d_get("a", 0), d_get("b", 0), d_get("c", 0)];
|
|
|
|
d.destroy();
|
|
|
|
d_get.destroy();
|
|
|
|
// l is [2, 3, 0].
|
|
|
|
pyodide.runPython(`
|
|
|
|
print(sys.getrefcount(d)) # prints 2
|
|
|
|
del d # runs destructor and prints "destructed!".
|
|
|
|
`);
|
|
|
|
```
|
|
|
|
Another exciting inconsistency is that `d.set` is a __Javascript__ method not a
|
|
|
|
PyProxy of a bound method, so using it has no effect on refcounts or memory
|
|
|
|
reclamation and it cannot be destroyed.
|
|
|
|
```pyodide
|
2021-03-24 11:05:00 +00:00
|
|
|
let d = pyodide.globals.get("d");
|
2021-03-14 10:15:53 +00:00
|
|
|
let d_set = d.set;
|
|
|
|
d_set("x", 7);
|
|
|
|
pyodide.runPython(`
|
|
|
|
print(sys.getrefcount(d)) # prints 2, d_set doesn't hold an extra reference to d
|
|
|
|
`);
|
|
|
|
d_set.destroy(); // TypeError: d_set.destroy is not a function
|
2018-06-24 16:29:46 +00:00
|
|
|
```
|
2018-06-21 15:19:34 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
## Explicit Conversion of Proxies
|
|
|
|
|
2021-03-25 16:26:07 +00:00
|
|
|
(type-translations-pyproxy-to-js)=
|
2021-03-14 10:15:53 +00:00
|
|
|
### Python to Javascript
|
|
|
|
Explicit conversion of a `PyProxy` into a native Javascript object is done with
|
|
|
|
the `toJs` method. By default, the `toJs` method does a recursive "deep"
|
|
|
|
conversion, to do a shallow conversion use `proxy.toJs(1)`. The `toJs` method
|
|
|
|
performs the following explicit conversions:
|
|
|
|
|
|
|
|
| Python | Javascript |
|
|
|
|
|------------------|---------------------|
|
|
|
|
| `list`, `tuple` | `Array` |
|
|
|
|
| `dict` | `Map` |
|
|
|
|
| `set` | `Set` |
|
|
|
|
|
|
|
|
In Javascript, `Map` and `Set` keys are compared using object identity unless
|
|
|
|
the key is an immutable type (meaning a string, a number, a bigint, a boolean,
|
|
|
|
`undefined`, or `null`). On the other hand, in Python, `dict` and `set` keys are
|
|
|
|
compared using deep equality. If a key is encountered in a `dict` or `set` that
|
|
|
|
would have different semantics in Javascript than in Python, then a
|
|
|
|
`ConversionError` will be thrown.
|
|
|
|
|
|
|
|
`````{admonition} Memory Leaks and toJs
|
|
|
|
:class: warning
|
|
|
|
|
|
|
|
The `toJs` method can create many proxies at arbitrary depth. It is your
|
|
|
|
responsibility to manually `destroy` these proxies if you wish to avoid memory
|
2021-03-24 11:05:00 +00:00
|
|
|
leaks, but we provide no way to manage this.
|
2021-03-14 10:15:53 +00:00
|
|
|
`````
|
|
|
|
|
|
|
|
To ensure that no `PyProxy` is leaked, the following code suffices:
|
|
|
|
```js
|
|
|
|
function destroyToJsResult(x){
|
|
|
|
if(!x){
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if(x.destroy){
|
|
|
|
x.destroy();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if(x[Symbol.iterator]){
|
|
|
|
for(let k of x){
|
|
|
|
freeToJsResult(k);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2018-06-21 15:19:34 +00:00
|
|
|
|
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
### Javascript to Python
|
|
|
|
Explicit conversion of a `JsProxy` into a native Python object is done with the
|
|
|
|
`to_py` method. By default, the `to_py` method does a recursive "deep"
|
|
|
|
conversion, to do a shallow conversion use `proxy.to_py(1)` The `to_py` method
|
|
|
|
performs the following explicit conversions:
|
|
|
|
|
|
|
|
| Javascript | Python |
|
|
|
|
|------------------|---------------------|
|
|
|
|
| `Array` | `list` |
|
|
|
|
| `Object`** | `dict` |
|
|
|
|
| `Map` | `dict` |
|
|
|
|
| `Set` | `set` |
|
|
|
|
|
|
|
|
** `to_py` will only convert an object into a dictionary if its constructor
|
|
|
|
is `Object`, otherwise the object will be left alone. Example:
|
|
|
|
```pyodide
|
|
|
|
class Test {};
|
|
|
|
window.x = { "a" : 7, "b" : 2};
|
|
|
|
window.y = { "a" : 7, "b" : 2};
|
|
|
|
Object.setPrototypeOf(y, Test.prototype);
|
|
|
|
pyodide.runPython(`
|
|
|
|
from js import x, y
|
|
|
|
# x is converted to a dictionary
|
2021-03-24 11:05:00 +00:00
|
|
|
assert x.to_py() == { "a" : 7, "b" : 2}
|
2021-03-14 10:15:53 +00:00
|
|
|
# y is not a "Plain Old JavaScript Object", it's an instance of type Test so it's not converted
|
|
|
|
assert y.to_py() == y
|
|
|
|
`);
|
2018-06-21 15:19:34 +00:00
|
|
|
```
|
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
In Javascript, `Map` and `Set` keys are compared using object identity unless
|
|
|
|
the key is an immutable type (meaning a string, a number, a bigint, a boolean,
|
|
|
|
`undefined`, or `null`). On the other hand, in Python, `dict` and `set` keys are
|
|
|
|
compared using deep equality. If a key is encountered in a `Map` or `Set` that
|
|
|
|
would have different semantics in Python than in Javascript, then a
|
|
|
|
`ConversionError` will be thrown. Also, in Javascript, `true !== 1` and `false
|
|
|
|
!== 0`, but in Python, `True == 1` and `False == 0`. This has the result that a
|
|
|
|
Javascript map can use `true` and `1` as distinct keys but a Python `dict`
|
|
|
|
cannot. If the Javascript map contains both `true` and `1` a `ConversionError`
|
|
|
|
will be thrown.
|
2018-06-21 15:19:34 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
## Buffers
|
2019-05-01 18:56:30 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
### Converting Javascript Typed Arrays to Python
|
2019-05-01 18:56:30 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
Javascript typed arrays (`Int8Array` and friends) are translated to Python
|
|
|
|
`memoryviews`. This happens with a single binary memory copy (since Python can't
|
|
|
|
directly access arrays if they are outside of the wasm heap), and the data type
|
|
|
|
is preserved. This makes it easy to correctly convert the array to a Numpy array
|
|
|
|
using `numpy.asarray`:
|
2019-05-01 18:56:30 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
```js
|
|
|
|
let array = new Float32Array([1, 2, 3]);
|
|
|
|
```
|
2019-05-01 18:56:30 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
```py
|
|
|
|
from js import array
|
|
|
|
import numpy as np
|
|
|
|
numpy_array = np.asarray(array)
|
2019-05-01 18:56:30 +00:00
|
|
|
```
|
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
### Converting Python Buffer objects to Javascript
|
2019-05-01 18:56:30 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
Python `bytes` and `buffer` objects are translated to Javascript as
|
|
|
|
`TypedArray`s without any memory copy at all. This conversion is thus very
|
|
|
|
efficient, but be aware that any changes to the buffer will be reflected in both
|
|
|
|
places.
|
|
|
|
|
|
|
|
Numpy arrays are currently converted to Javascript as nested (regular) Arrays. A
|
|
|
|
more efficient method will probably emerge as we decide on an ndarray
|
|
|
|
implementation for Javascript.
|
|
|
|
|
|
|
|
|
|
|
|
## Importing Python objects into Javascript
|
|
|
|
|
|
|
|
A Python object in the `__main__` global scope can imported into Javascript
|
2021-03-24 11:05:00 +00:00
|
|
|
using the `pyodide.globals.get` method. Given the name of the Python object
|
|
|
|
to import, it returns the object translated to Javascript.
|
2021-03-14 10:15:53 +00:00
|
|
|
|
|
|
|
```js
|
2021-03-24 11:05:00 +00:00
|
|
|
let sys = pyodide.globals.get('sys');
|
2019-05-01 18:56:30 +00:00
|
|
|
```
|
|
|
|
|
2021-03-24 11:05:00 +00:00
|
|
|
As always, if the result is a `PyProxy` and you care about not leaking the Python
|
2021-03-14 10:15:53 +00:00
|
|
|
object, you must destroy it when you are done.
|
|
|
|
|
|
|
|
(type-translations_using-js-obj-from-py)=
|
|
|
|
## Importing Javascript objects into Python
|
|
|
|
|
|
|
|
Javascript objects in the
|
|
|
|
[`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis)
|
|
|
|
global scope can be imported into Python using the `js` module.
|
2019-05-01 18:56:30 +00:00
|
|
|
|
2021-03-14 10:15:53 +00:00
|
|
|
When importing a name from the `js` module, the `js` module looks up Javascript
|
|
|
|
attributes of the `globalThis` scope and translates the Javascript objects into
|
|
|
|
Python. You can create your own custom Javascript modules using
|
|
|
|
{any}`pyodide.registerJsModule`.
|
|
|
|
|
|
|
|
```py
|
|
|
|
import js
|
|
|
|
js.document.title = 'New window title'
|
|
|
|
from js.document.location import reload as reload_page
|
|
|
|
reload_page()
|
2018-06-21 15:19:34 +00:00
|
|
|
```
|