rapydscript-ns
v0.9.2
Published
Pythonic JavaScript that doesn't suck
Maintainers
Readme
RapydScript
RapydScript is a pre-compiler for Javascript that uses syntax identical to modern Python. It transpiles to native JS (with source maps) that reads like your Python code, but runs natively in the browser or node. Try RapydScript-ns live via an in-browser REPL!
This is a fork of the original RapydScript that adds many new features. The most notable change is that all the Python features that are optional in RapydScript are now enabled by default.
Contents
- What is RapydScript?
- Installation
- Compilation
- Getting Started
- Leveraging other APIs
- Anonymous Functions
- Lambda Expressions
- Decorators
- Self-Executing Functions
- Chaining Blocks
- Function calling with optional arguments
- Inferred Tuple Packing/Unpacking
- Operators and keywords
- Literal JavaScript
- Containers (lists/sets/dicts)
- Loops
- List/Set/Dict Comprehensions
- Strings
- The Existential Operator
- Walrus Operator
- Ellipsis Literal
- Extended Subscript Syntax
- Variable Type Annotations
- Regular Expressions
- JSX Support
- Creating DOM trees easily
- Classes
- Iterators
- Generators
- Modules
- Structural Pattern Matching
- Exception Handling
- Scope Control
- Available Libraries
- Linter
- Making RapydScript even more pythonic
- Advanced Usage Topics
- Internationalization
- Gotchas
- Monaco Language Service
- Python Feature Coverage
- Reasons for the fork
What is RapydScript?
RapydScript (pronounced 'RapidScript') is a pre-compiler for JavaScript, similar to CoffeeScript, but with cleaner, more readable syntax. The syntax is almost identical to Python, but RapydScript has a focus on performance and interoperability with external JavaScript libraries. This means that the JavaScript that RapydScript generates is performant and quite close to hand- written JavaScript.
RapydScript allows to write your front-end in Python without the overhead that other similar frameworks introduce - the performance is the same as with pure JavaScript. To those familiar with CoffeeScript, RapydScript is similar, but inspired by Python's readability rather than Ruby's cleverness. To those familiar with Pyjamas, RapydScript brings many of the same features and support for Python syntax without the same overhead. RapydScript combines the best features of Python as well as JavaScript, bringing you features most other Pythonic JavaScript replacements overlook. Here are a few features of RapydScript:
- classes that work and feel similar to Python
- an import system for modules/packages that works just like Python's
- optional function arguments that work similar to Python
- inheritance system that's both, more powerful than Python and cleaner than JavaScript
- support for object literals with anonymous functions, like in JavaScript
- ability to invoke any JavaScript/DOM object/function/method as if it's part of the same framework, without the need for special syntax
- variable and object scoping that make sense (no need for repetitive 'var' or 'new' keywords)
- ability to use both, Python's methods/functions and JavaScript's alternatives
- similar to above, ability to use both, Python's and JavaScript's tutorials (as well as widgets)
- it's self-hosting, that means the compiler is itself written in RapydScript and compiles into JavaScript
Installation
Try RapydScript-ns live via an in-browser REPL!
First make sure you have installed the latest version of node.js (You may need to restart your computer after this step).
From NPM for use as a command line app:
npm install rapydscript-ns -gFrom NPM for use in your own node project:
npm install rapydscript-nsFrom Git:
git clone https://github.com/ficocelliguy/rapydscript-ns.git
cd rapydscript-ns
sudo npm link .
npm install # This will automatically install the dependencies for RapydScriptIf you're using OSX, you can probably use the same commands (let me know if that's not the case). If you're using Windows, you should be able to follow similar commands after installing node.js, npm and git on your system.
Compilation
Once you have installed RapydScript, compiling your application is as simple as running the following command:
rapydscript [options] <location of main file>By default this will dump the output to STDOUT, but you can specify the output
file using --output option. The generated file can then be referenced in your
html page the same way as you would with a typical JavaScript file. If you're
only using RapydScript for classes and functions, then you're all set. For more
help, use rapydscript -h.
Getting Started
RapydScript comes with its own Read-Eval-Print-Loop (REPL). Just run
rapydscript without any arguments to get started trying out the code
snippets below.
Like JavaScript, RapydScript can be used to create anything from a quick function to a complex web-app. RapydScript can access anything regular JavaScript can, in the same manner. Let's say we want to write a function that greets us with a "Hello World" pop-up. The following code will do it:
def greet():
alert("Hello World!")Once compiled, the above code will turn into the following JavaScript:
function greet() {
alert("Hello World!");
}Now you can reference this function from other JavaScript or the page itself (using "onclick", for example). For our next example, let's say you want a function that computes factorial of a number:
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)Now all we need is to tie it into our page so that it's interactive. Let's add an input field to the page body and a cell for displaying the factorial of the number in the input once the input loses focus.
<input id="user-input" onblur="computeFactorial()"></input>
<div id="result"></div>NOTE: To complement RapydScript, I have also written RapydML (https://bitbucket.org/pyjeon/rapydml), which is a pre-compiler for HTML (just like RapydScript is a pre-compiler for JavaScript).
Now let's implement computeFactorial() function in RapydScript:
def computeFactorial():
n = document.getElementById("user-input").value
document.getElementById("result").innerHTML = factorial(n)Again, notice that we have access to everything JavaScript has access to, including direct DOM manipulation. Once compiled, this function will look like this:
function computeFactorial() {
var n;
n = document.getElementById("user-input").value;
document.getElementById("result").innerHTML = factorial(n);
}Notice that RapydScript automatically declares variables in local scope when
you try to assign to them. This not only makes your code shorter, but saves you
from making common JavaScript mistake of overwriting a global. For more
information on controlling variable scope, see Scope Control section.
Leveraging other APIs
Aside from Python-like stdlib, RapydScript does not have any of its own APIs. Nor does it need to, there are already good options available that we can leverage instead. If we wanted, for example, to rewrite the above factorial logic using jQuery, we could easily do so:
def computeFactorial():
n = $("#user-input").val()
$("#result").text(factorial(n))Many of these external APIs, however, take object literals as input. Like with JavaScript, you can easily create those with RapydScript, the same way you would create a dictionary in Python:
styles = {
'background-color': '#ffe',
'border-left': '5px solid #ccc',
'width': 50,
}Now you can pass it to jQuery:
$('#element').css(styles)Another feature of RapydScript is ability to have functions as part of your object literal. JavaScript APIs often take callback/handler functions as part of their input parameters, and RapydScript lets you create such object literal without any quirks/hacks:
params = {
'width': 50,
'height': 30,
'onclick': def(event):
alert("you clicked me"),
'onmouseover': def(event):
$(this).css('background', 'red')
,
'onmouseout': def(event):
# reset the background
$(this).css('background', '')
}Note the comma on a new line following a function declaration, it needs to be
there to let the compiler know there are more attributes in this object
literal, yet it can't go on the same line as the function since it would get
parsed as part of the function block. Like Python, however, RapydScript
supports new-line shorthand using a ;, which you could use to place the comma
on the same line:
hash = {
'foo': def():
print('foo');,
'bar': def():
print('bar')
}It is because of easy integration with JavaScript's native libraries that RapydScript keeps its own libraries to a minimum.
Anonymous Functions
Like JavaScript, RapydScript allows the use of anonymous functions. In fact,
you've already seen the use of anonymous functions in previous section when
creating an object literal ('onmouseover' and 'onmouseout' assignments). Unlike
Python's lambda, anonymous functions created with def are not limited to a
single expression. The following two function declarations are equivalent:
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
factorial = def(n):
if n == 0:
return 1
return n * factorial(n-1)This might not seem like much at first, but if you're familiar with JavaScript, you know that this can be extremely useful to the programmer, especially when dealing with nested functions, which are a bit syntactically awkward in Python (it's not immediately obvious that those can be copied and assigned to other objects). To illustrate the usefulness, let's create a method that creates and returns an element that changes color while the user keeps the mouse pressed on it.
def makeDivThatTurnsGreen():
div = $('<div></div>')
turnGreen = def(event):
div.css('background', 'green')
div.mousedown(turnGreen)
resetColor = def(event):
div.css('background', '')
div.mouseup(resetColor)
return divAt first glance, anonymous functions might not seem that useful. We could have easily created nested functions and assigned them instead. By using anonymous functions, however, we can quickly identify that these functions will be bound to a different object. They belong to the div, not the main function that created them, nor the logic that invoked it. The best use case for these is creating an element inside another function/object without getting confused which object the function belongs to.
Additionally, as you already noticed in the previous section, anonymous functions can be used to avoid creating excessive temporary variables and make your code cleaner:
math_ops = {
'add': def(a, b): return a+b;,
'sub': def(a, b): return a-b;,
'mul': def(a, b): return a*b;,
'div': def(a, b): return a/b;,
'roots': def(a, b, c):
r = Math.sqrt(b*b - 4*a*c)
d = 2*a
return (-b + r)/d, (-b - r)/d
}Note that the example puts the function header (def()) and content on the same
line (function inlining). This is a feature of RapydScript that can be used
to make the code cleaner in cases like the example above. You can also use it
in longer functions by chaining statements together using ;.
Lambda Expressions
RapydScript supports Python's lambda keyword for creating single-expression
anonymous functions inline. The syntax is identical to Python:
lambda arg1, arg2, ...: expressionThe body must be a single expression whose value is implicitly returned. For
multi-statement bodies, use def instead.
# Simple lambda assigned to a variable
double = lambda x: x * 2
double(5) # → 10
# Lambda with multiple arguments
add = lambda a, b: a + b
add(3, 4) # → 7
# Lambda with no arguments
forty_two = lambda: 42
# Lambda with a ternary body
abs_val = lambda x: x if x >= 0 else -x
abs_val(-5) # → 5
# Lambda used inline (e.g. as a sort key)
nums = [3, 1, 2]
nums.sort(lambda a, b: a - b)
# nums is now [1, 2, 3]
# Lambda with a default argument value
greet = lambda name='world': 'hello ' + name
greet() # → 'hello world'
greet('alice') # → 'hello alice'
# Lambda with *args
total = lambda *args: sum(args)
total(1, 2, 3) # → 6
# Closure: lambda captures variables from the enclosing scope
def make_adder(n):
return lambda x: x + n
add5 = make_adder(5)
add5(3) # → 8
# Nested lambdas
mult = lambda x: lambda y: x * y
mult(3)(4) # → 12lambda is purely syntactic sugar — lambda x: expr compiles to the same
JavaScript as def(x): return expr. Use def when the body spans multiple
lines or needs statements.
Decorators
Like Python, RapydScript supports decorators.
def makebold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped
def makeitalic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped
@makebold
@makeitalic
def hello():
return "hello world"
hello() # returns "<b><i>hello world</i></b>"Class decorators are also supported with the caveat that the class properties must be accessed via the prototype property. For example:
def add_x(cls):
cls.prototype.x = 1
@add_x
class A:
pass
print(A.x) # will print 1Self-Executing Functions
RapydScript wouldn't be useful if it required work-arounds for things that JavaScript handled easily. If you've worked with JavaScript or jQuery before, you've probably seen the following syntax:
(function(args){
// some logic here
})(args)This code calls the function immediately after declaring it instead of assigning it to a variable. Python doesn't have any way of doing this. The closest work-around is this:
def tmp(args):
# some logic here
tmp.__call__(args)While it's not horrible, it did litter our namespace with a temporary variable. If we have to do this repeatedly, this pattern does get annoying. This is where RapydScript decided to be a little unorthodox and implement the JavaScript-like solution:
(def(args):
# some logic here
)()A close cousin of the above is the following code (passing current scope to the function being called):
function(){
// some logic here
}.call(this);With RapydScript equivalent of:
def():
# some logic here
.call(this)There is also a third alternative, that will pass the arguments as an array:
def(a, b):
# some logic here
.apply(this, [a, b])Chaining Blocks
As seen in previous section, RapydScript will bind any lines beginning with . to the outside of the block with the matching indentation. This logic isn't limited to the .call() method, you can use it with .apply() or any other method/property the function has assigned to it. This can be used for jQuery as well:
$(element)
.css('background-color', 'red')
.show()The only limitation is that the indentation has to match, if you prefer to indent your chained calls, you can still do so by using the \ delimiter:
$(element)\
.css('background-color', 'red')\
.show()This feature handles do/while loops as well:
a = 0
do:
print(a)
a += 1
.while a < 1Function calling with optional arguments
RapydScript supports the same function calling format as Python. You can have named optional arguments, create functions with variable numbers of arguments and variable numbers of named arguments. Some examples will illustrate this best:
def f1(a, b=2):
return [a, b]
f1(1, 3) == f1(1, b=3) == [1, 3]
def f2(a, *args):
return [a, args]
f2(1, 2, 3) == [1, [2, 3]]
def f3(a, b=2, **kwargs):
return [a, b, kwargs]
f3(1, b=3, c=4) == [1, 3, {c:4}]
def f4(*args, **kwargs):
return [args, kwargs]
f4(1, 2, 3, a=1, b=2):
return [[1, 2, 3], {a:1, b:2}]Positional-only and keyword-only parameters
RapydScript supports Python's / and * parameter separators:
/(positional-only separator): parameters listed before/can only be passed positionally — they cannot be named at the call site.*(keyword-only separator): parameters listed after a bare*can only be passed by name — they cannot be passed positionally.
def greet(name, /, greeting="Hello", *, punctuation="!"):
return greeting + ", " + name + punctuation
greet("Alice") # Hello, Alice!
greet("Bob", greeting="Hi") # Hi, Bob!
greet("Carol", punctuation=".") # Hello, Carol.
greet("Dave", greeting="Hey", punctuation="?") # Hey, Dave?
# name is positional-only: greet(name="Alice") would silently ignore the kwarg
# punctuation is keyword-only: must be passed as punctuation="."The two separators can be combined, and each section can have its own default values. All combinations supported by Python 3.8+ are accepted.
RapydScript is lenient: passing a positional-only parameter by keyword will not
raise a TypeError at runtime (the named value is silently ignored), and
passing a keyword-only parameter positionally will not raise an error either.
This is consistent with RapydScript's general approach of favouring
interoperability over strict enforcement.
The Monaco language service correctly shows / and * separators in
signature help and hover tooltips.
One difference between RapydScript and Python is that RapydScript is not as
strict as Python when it comes to validating function arguments. This is both
for performance and to make it easier to interoperate with other JavaScript
libraries. So if you do not pass enough arguments when calling a function, the
extra arguments will be set to undefined instead of raising a TypeError, as in
Python. Similarly, when mixing *args and optional arguments, RapydScript
will not complain if an optional argument is specified twice.
When creating callbacks to pass to other JavaScript libraries, it is often the case that the external library expects a function that receives an options object as its last argument. There is a convenient decorator in the standard library that makes this easy:
@options_object
def callback(a, b, opt1=default1, opt2=default2):
console.log(opt1, opt2)
callback(1, 2, {'opt1':'x', 'opt2':'y'}) # will print x, yNow when you pass callback into the external library and it is called with an object containing options, they will be automatically converted by RapydScript into the names optional parameters you specified in the function definition.
Inferred Tuple Packing/Unpacking
Like Python, RapydScript allows inferred tuple packing/unpacking and assignment. For example, if you wanted to swap two variables, the following is simpler than explicitly declaring a temporary variable:
a, b = b, aLikewise, if a function returns multiple variables, it's cleaner to say:
a, b, c = fun()rather than:
tmp = fun()
a = tmp[0]
b = tmp[1]
c = tmp[2]Since JavaScript doesn't have tuples, RapydScript uses arrays for tuple packing/unpacking behind the scenes, but the functionality stays the same. Note that unpacking only occurs when you're assigning to multiple arguments:
a, b, c = fun() # gets unpacked
tmp = fun() # no unpacking, tmp will store an array of length 3Unpacking can also be done in for loops (which you can read about in later section):
for index, value in enumerate(items):
print(index+': '+value)Tuple packing is the reverse operation, and is done to the variables being assigned, rather than the ones being assigned to. This can occur during assignment or function return:
def fun():
return 1, 2, 3To summarize packing and unpacking, it's basically just syntax sugar to remove obvious assignment logic that would just litter the code. For example, the swap operation shown in the beginning of this section is equivalent to the following code:
tmp = [b, a]
a = tmp[0]
b = tmp[1]RapydScript also supports Python's starred assignment (a, *b, c = iterable), which collects the remaining elements into a list. The starred variable can appear at any position — front, middle, or end:
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4] # init=[1, 2, 3], last=4
head, *mid, tail = [1, 2, 3, 4, 5] # head=1, mid=[2, 3, 4], tail=5Starred assignment works with any iterable, including generators and strings (which are unpacked character by character). The starred variable always receives a list, even if it captures zero elements.
Explicit tuple literals using parentheses work the same as in Python and compile to JavaScript arrays:
empty = () # []
single = (42,) # [42] — trailing comma required for single-element tuple
pair = (1, 2) # [1, 2]
triple = ('a', 'b', 'c') # ['a', 'b', 'c']
nested = ((1, 2), (3, 4)) # [[1, 2], [3, 4]]A parenthesised expression without a trailing comma is not a tuple — (x) is just x. Add a comma to make it one: (x,).
Tuple literals work naturally everywhere arrays do: as return values, function arguments, in isinstance checks, and in destructuring assignments:
def bounding_box(points):
return (min(p[0] for p in points), max(p[0] for p in points))
ok = isinstance(value, (int, str)) # tuple of types
(a, b), c = (1, 2), 3Operators and keywords
RapydScript uses the python form for operators and keywords. Below is the mapping from RapydScript to JavaScript.
Keywords:
RapydScript JavaScript
None null
False false
True true
... ρσ_Ellipsis (the Ellipsis singleton)
undefined undefined
this thisOperators:
RapydScript JavaScript
and &&
or ||
not !
is ===
is not !==
+=1 ++
-=1 --
** Math.pow()
**= x = Math.pow(x, y)All Python augmented assignment operators are supported: +=, -=, *=, /=, //=, **=, %=, >>=, <<=, |=, ^=, &=.
Admittedly, is is not exactly the same thing in Python as === in JavaScript, but JavaScript is quirky when it comes to comparing objects anyway.
Literal JavaScript
In rare cases RapydScript might not allow you to do what you need to, and you
need access to pure JavaScript, this is particularly useful for performance
optimizations in inner loops. When that's the case, you can use a verbatim
string literal. That is simply a normal RapydScript string prefixed with the
v character. Code inside a verbatim string literal is not a sandbox, you
can still interact with it from normal RapydScript:
v'a = {foo: "bar", baz: 1};'
print(a.foo) # prints "bar"
for v'i = 0; i < arr.length; i++':
print (arr[i])Containers (lists/sets/dicts)
Lists
Lists in RapydScript are almost identical to lists in Python, but are also
native JavaScript arrays. The sort() and pop() methods behave exactly
as in Python: sort() performs a numeric sort (in-place, with optional key
and reverse arguments) and pop() performs a bounds-checked pop (raises
IndexError for out-of-bounds indices). If you need the native JavaScript
behavior for interop with external JS libraries, use jssort() (lexicographic
sort) and jspop() (no bounds check, always pops the last element). The old
pysort() and pypop() names are kept as backward-compatible aliases.
Note that even list literals in RapydScript create Python-like list objects,
and you can also use the builtin list() function to create lists from other
iterable objects, just as you would in Python. You can create a RapydScript
list from a plain native JavaScript array by using the list_wrap() function,
like this:
a = v'[1, 2]'
pya = list_wrap(a)
# Now pya is a python like list object that satisfies pya === aList Concatenation
The + operator concatenates two lists and returns a new list, exactly as in Python:
a = [1, 2]
b = [3, 4]
c = a + b # [1, 2, 3, 4] — a and b are unchangedThe += operator extends a list in-place (the original list object is mutated):
a = [1, 2]
ref = a # ref and a point to the same list
a += [3, 4] # mutates a in-place
print(ref) # [1, 2, 3, 4] — ref sees the updateNo special flag is required. The + operator compiles to a lightweight helper
(ρσ_list_add) that uses Array.concat for lists and falls back to native JS
+ for numbers and strings.
Sets
Sets in RapydScript are identical to those in python. You can create them using
set literals or comprehensions and all set operations are supported. You can
store any object in a set. For primitive types (strings, numbers) the value is
used for equality; for class instances, object identity (is) is used by
default unless the class defines a __hash__ method.
Note that sets are not a subclass of the ES 6 JavaScript Set object, however, they do use this object as a backend, when available. You can create a set from any enumerable container, like you would in python
s = set(list or other set or string)You can also wrap an existing JavaScript Set object efficiently, without creating a copy with:
js_set = Set()
py_set = set_wrap(js_set)Note that using non-primitive objects as set members does not behave the same way as in Python. For example:
a = [1, 2]
s = {a}
a in s # True
[1, 2] in s # FalseThis is because list identity (not value) determines set membership for mutable
objects. Define __hash__ on your own classes to control set/dict membership.
Dicts
dicts are the most different in RapydScript, from Python. This is because RapydScript uses the JavaScript Object as a dict, for compatibility with external JavaScript libraries and performance. This means there are several differences between RapydScript dicts and Python dicts.
- You can only use primitive types (strings/numbers) as keys in the dict
- If you use numbers as keys, they are auto-converted to strings
- You can access the keys of the dict as attributes of the dict object
- Trying to access a non-existent key returns ``undefined`` instead of
raising a KeyError
- dict objects do not have the same methods as python dict objects:
``items(), keys(), values(), get(), pop(), etc.`` You can however use
RapydScript dict objects in ```for..in``` loops.Fortunately, there is a builtin dict type that behaves just like Python's
dict with all the same methods. The dict_literals and
overload_getitem flags are on by default, so dict literals and the
[] operator already behave like Python:
a = {1:1, 2:2}
a[1] # == 1
a[3] = 3
list(a.keys()) == [1, 2, 3]
a['3'] # raises a KeyError as this is a proper python dict, not a JavaScript objectThese are scoped flags — local to the scope where they appear. You can
disable them for a region of code using the no_ prefix:
a = {1:1, 2:2}
isinstance(a, dict) == True
from __python__ import no_dict_literals, no_overload_getitem
a = {1:1, 2:2}
isinstance(a, dict) == False # a is a normal JavaScript objectList spread literals
RapydScript supports Python's *expr spread syntax inside list literals.
One or more *expr items can appear anywhere, interleaved with ordinary
elements:
a = [1, 2, 3]
b = [4, 5]
# Spread at the end
result = [0, *a] # [0, 1, 2, 3]
# Spread in the middle
result = [0, *a, *b, 6] # [0, 1, 2, 3, 4, 5, 6]
# Copy a list
copy = [*a] # [1, 2, 3]
# Unpack a string
chars = [*'hello'] # ['h', 'e', 'l', 'l', 'o']Spread works on any iterable (lists, strings, generators, range()).
The result is always a new Python list. Translates to JavaScript's
[...expr] spread syntax.
Set spread literals
The same *expr syntax works inside set literals {...}:
a = [1, 2, 3]
b = [3, 4, 5]
s = {*a, *b} # set([1, 2, 3, 4, 5]) — duplicates removed
s2 = {*a, 10} # set([1, 2, 3, 10])Translates to ρσ_set([...a, ...b]).
**expr in function calls
**expr in a function call now accepts any expression, not just a plain
variable name:
def f(x=0, y=0):
return x + y
opts = {'x': 10, 'y': 20}
f(**opts) # 30 (variable — always worked)
f(**{'x': 1, 'y': 2}) # 3 (dict literal)
f(**cfg.defaults) # uses attribute access result
f(**get_opts()) # uses function call resultDict merge literals
RapydScript supports Python's {**d1, **d2} dict merge (spread) syntax.
One or more **expr items can appear anywhere inside a {...} literal,
interleaved with ordinary key: value pairs:
defaults = {'color': 'blue', 'size': 10}
overrides = {'size': 20}
# Merge two dicts — later items win
merged = {**defaults, **overrides}
# merged == {'color': 'blue', 'size': 20}
# Mix spread with literal key-value pairs
result = {**defaults, 'weight': 5}
# result == {'color': 'blue', 'size': 10, 'weight': 5}This works for both plain JavaScript-object dicts and Python dict objects
(dict_literals is on by default):
pd1 = {'a': 1}
pd2 = {'b': 2}
merged = {**pd1, **pd2} # isinstance(merged, dict) == TrueThe spread items are translated using Object.assign for plain JS objects
and dict.update() for Python dicts.
Dict merge operator | and |= (Python 3.9+)
Python dicts support the | (merge) and |= (update in-place) operators
(requires overload_operators and dict_literals, both on by default):
d1 = {'x': 1, 'y': 2}
d2 = {'y': 99, 'z': 3}
# Create a new merged dict — right-side values win on key conflict
merged = d1 | d2 # {'x': 1, 'y': 99, 'z': 3}
# Update d1 in place
d1 |= d2 # d1 is now {'x': 1, 'y': 99, 'z': 3}d1 | d2 creates a new dict (neither operand is mutated).
d1 |= d2 merges d2 into d1 and returns d1.
Without overload_operators the | symbol is bitwise OR — use
{**d1, **d2} spread syntax as an alternative if the flag is disabled.
Arithmetic operator overloading
RapydScript supports Python-style arithmetic operator overloading via the
overload_operators flag, which is on by default:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __neg__(self):
return Vector(-self.x, -self.y)
a = Vector(1, 2)
b = Vector(3, 4)
c = a + b # calls a.__add__(b) → Vector(4, 6)
d = -a # calls a.__neg__() → Vector(-1, -2)The supported dunder methods are:
| Expression | Forward method | Reflected method |
|------------|-----------------|-------------------|
| a + b | __add__ | __radd__ |
| a - b | __sub__ | __rsub__ |
| a * b | __mul__ | __rmul__ |
| a / b | __truediv__ | __rtruediv__ |
| a // b | __floordiv__ | __rfloordiv__ |
| a % b | __mod__ | __rmod__ |
| a ** b | __pow__ | __rpow__ |
| a & b | __and__ | __rand__ |
| a \| b | __or__ | __ror__ |
| a ^ b | __xor__ | __rxor__ |
| a << b | __lshift__ | __rlshift__ |
| a >> b | __rshift__ | __rrshift__ |
| -a | __neg__ | |
| +a | __pos__ | |
| ~a | __invert__ | |
Augmented assignment (+=, -=, etc.) first tries the in-place method
(__iadd__, __isub__, …) and then falls back to the binary method.
If neither operand defines the relevant dunder method, the operation enforces Python-style type compatibility before falling back to the native JavaScript operator:
number±number→ allowed (includingbool, which is treated as an integer subclass)str+str→ allowedstr*intandlist*int→ allowed (and also withboolin place ofint)- Anything else raises
TypeErrorwith a Python-style message, e.g.:
1 + 'x' # TypeError: unsupported operand type(s) for +: 'int' and 'str'
'a' - 1 # TypeError: unsupported operand type(s) for -: 'str' and 'int'
[1] + 'b' # TypeError: unsupported operand type(s) for +: 'list' and 'str'This type-checking is controlled by the strict_arithmetic flag, which is
on by default when overload_operators is active. To revert to
JavaScript's silent coercion behaviour (e.g. 1 + 'x' → '1x') without
disabling dunder dispatch, use:
from __python__ import no_strict_arithmeticWhen overload_operators is
disabled (from __python__ import no_overload_operators) the operators
compile directly to JavaScript and no type checking is performed.
When overload_operators is active, string and list repetition with * works just like Python:
'ha' * 3 # 'hahaha'
3 * 'ha' # 'hahaha'
[0] * 4 # [0, 0, 0, 0]
[1, 2] * 2 # [1, 2, 1, 2]Because the dispatch adds one or two property lookups per operation, you can
disable it in scopes where it is not needed with
from __python__ import no_overload_operators.
The collections.Counter class defines __add__, __sub__, __or__,
and __and__. With overload_operators you can use the natural operator
syntax:
from collections import Counter
c1 = Counter('aab')
c2 = Counter('ab')
c3 = c1 + c2 # {'a': 3, 'b': 2}
c4 = c1 - c2 # {'a': 1}
c5 = c1 | c2 # union (max) → {'a': 2, 'b': 1}
c6 = c1 & c2 # intersection (min) → {'a': 1, 'b': 1}Container comparisons
Container equality (the == and != operators) work for lists and sets and
RapydScript dicts (but not arbitrary javascript objects). You can also define
the __eq__(self, other) method in your classes to have these operators work
for your own types.
The ordering operators <, >, <=, >= dispatch to Python-style
dunder methods and compare lists lexicographically — just like Python:
from __python__ import overload_operators # on by default
# List comparison — lexicographic order
assert [1, 2] < [1, 3] # True (first differing element: 2 < 3)
assert [1, 2] < [1, 2, 0] # True (prefix is smaller)
assert [2] > [1, 99] # True (first element dominates)
# Works with custom __lt__ / __gt__ / __le__ / __ge__ on objects
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
v1 = Version(1, 5)
v2 = Version(2, 0)
assert v1 < v2 # dispatches to __lt__
# Incompatible types raise TypeError, just like Python
try:
result = [1] < 42
except TypeError as e:
print(e) # '<' not supported between instances of 'list' and 'int'Chained comparisons work just like Python — each middle operand is evaluated only once:
# All of these work correctly, including mixed-direction chains
assert 1 < 2 < 3 # True
assert 1 < 2 > 0 # True (1<2 AND 2>0)
assert [1] < [2] < [3] # True (lexicographic chain)Python Truthiness and __bool__
RapydScript uses Python truthiness semantics by default (truthiness is
on by default):
When this flag is active:
- Empty containers are falsy:
[],{},set(),'',0,Noneare all falsy. __bool__is dispatched: objects with a__bool__method control their truthiness.__len__is used: objects with__len__are falsy whenlen(obj) == 0.and/orreturn operand values (not booleans), just like Python.- All condition positions (
if,while,assert,not, ternary) use Python semantics.
class Empty:
def __bool__(self): return False
if not []: # True — [] is falsy
print('empty')
x = [] or 'default' # x == 'default'
y = [1] or 'default' # y == [1]
z = [1] and 'ok' # z == 'ok'The flag is scoped — it applies until the end of the enclosing
function or class body. Use from __python__ import no_truthiness to
disable it in a sub-scope where JavaScript truthiness is needed.
Callable Objects (__call__)
Any class that defines __call__ can be invoked directly with obj(args),
just like Python callable objects:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return self.factor * x
triple = Multiplier(3)
triple(7) # 21 — dispatches to triple.__call__(7)callable(obj) returns True when __call__ is defined. The dispatch is
automatic for all direct function-call expressions that are simple names
(i.e. not method accesses like obj.method()).
frozenset
RapydScript provides a full frozenset builtin — an immutable, unordered
collection of unique elements, identical to Python's frozenset.
fs = frozenset([1, 2, 3])
len(fs) # 3
2 in fs # True
isinstance(fs, frozenset) # True
# Set operations return new frozensets
a = frozenset([1, 2, 3])
b = frozenset([2, 3, 4])
a.union(b) # frozenset({1, 2, 3, 4})
a.intersection(b) # frozenset({2, 3})
a.difference(b) # frozenset({1})
a.symmetric_difference(b) # frozenset({1, 4})
a.issubset(frozenset([1, 2, 3, 4])) # True
a.issuperset(frozenset([1, 2])) # True
a.isdisjoint(frozenset([5, 6])) # True
# Compares equal to a set with the same elements
frozenset([1, 2]).__eq__({1, 2}) # TrueMutation methods (add, remove, discard, clear, update) are not
present on frozenset instances, enforcing immutability at the API level.
frozenset objects can be iterated and copied with .copy().
bytes and bytearray
RapydScript provides bytes (immutable) and bytearray (mutable) builtins
that match Python's semantics and are backed by plain JS arrays of integers
in the range 0–255.
b'...' bytes literals
RapydScript supports Python b'...' bytes literal syntax. The prefix may be
b or B (and rb/br for raw bytes where backslash sequences are not
interpreted). Adjacent bytes literals are automatically concatenated, just
like adjacent string literals.
b'Hello' # bytes([72, 101, 108, 108, 111])
b'\x00\xff' # bytes([0, 255]) — hex escape sequences work
b'\n\t\r' # bytes([10, 9, 13]) — control-char escapes work
b'foo' b'bar' # bytes([102, 111, 111, 98, 97, 114]) — concatenation
rb'\n\t' # bytes([92, 110, 92, 116]) — raw: backslashes literal
B'ABC' # bytes([65, 66, 67]) — uppercase B also acceptedEach b'...' literal is compiled to a bytes(str, 'latin-1') call, so the
full bytes API is available on the result.
Construction
bytes() # empty bytes
bytes(4) # b'\x00\x00\x00\x00' (4 zero bytes)
b'\x00\x00\x00\x00' # same — bytes literal syntax
bytes([72, 101, 108, 111]) # b'Hello'
b'Hell\x6f' # same — mix of ASCII and hex escapes
bytes('Hello', 'utf-8') # encode a string
bytes('ABC', 'ascii') # ASCII / latin-1 encoding also accepted
bytes.fromhex('48656c6c6f') # from hex string → b'Hello'
bytearray() # empty mutable byte sequence
bytearray(3) # bytearray(b'\x00\x00\x00')
bytearray([1, 2, 3]) # from list of ints
bytearray('Hi', 'utf-8') # from string
bytearray(some_bytes) # mutable copy of a bytes objectUint8Array values may also be passed as the source argument.
Common operations (both bytes and bytearray)
b = bytes('Hello', 'utf-8')
len(b) # 5
b[0] # 72 (integer)
b[-1] # 111
b[1:4] # bytes([101, 108, 108]) (slice → new bytes)
b[::2] # every other byte
b + bytes([33]) # concatenate → b'Hello!'
b * 2 # repeat → b'HelloHello'
72 in b # True (integer membership)
bytes([101, 108]) in b # True (subsequence membership)
b == bytes([72, 101, 108, 108, 111]) # True
b.hex() # '48656c6c6f'
b.hex(':', 2) # '48:65:6c:6c:6f' (separator every 2 bytes)
b.decode('utf-8') # 'Hello'
b.decode('ascii') # works for ASCII-range bytes
b.find(bytes([108, 108])) # 2
b.index(101) # 1
b.rfind(108) # 3
b.count(108) # 2
b.startswith(bytes([72])) # True
b.endswith(bytes([111])) # True
b.split(bytes([108])) # [b'He', b'', b'o']
b.replace(bytes([108]), bytes([76])) # b'HeLLo'
b.strip() # strip leading/trailing whitespace bytes
b.upper() # b'HELLO'
b.lower() # b'hello'
bytes(b' ').join([bytes('a', 'ascii'), bytes('b', 'ascii')]) # b'a b'
repr(b) # "b'Hello'"
isinstance(b, bytes) # True
isinstance(bytearray([1]), bytes) # True (bytearray is a subclass of bytes)bytearray-only mutation methods
ba = bytearray([1, 2, 3])
ba[0] = 99 # item assignment
ba[1:3] = bytes([20, 30]) # slice assignment
ba.append(4) # add one byte
ba.extend([5, 6]) # add multiple bytes
ba.insert(0, 0) # insert at index
ba.pop() # remove and return last byte (or ba.pop(i))
ba.remove(20) # remove first occurrence of value
ba.reverse() # reverse in place
ba.clear() # remove all bytes
ba += bytearray([7, 8]) # in-place concatenationissubclass
issubclass(cls, classinfo) checks whether a class is a subclass of another
class (or any class in a tuple of classes). Every class is considered a
subclass of itself.
class Animal: pass
class Dog(Animal): pass
class Poodle(Dog): pass
class Cat(Animal): pass
issubclass(Dog, Animal) # True
issubclass(Poodle, Animal) # True — transitive
issubclass(Poodle, Dog) # True
issubclass(Dog, Dog) # True — a class is its own subclass
issubclass(Cat, Dog) # False
issubclass(Animal, Dog) # False — parent is not a subclass of child
# tuple form — True if cls is a subclass of any entry
issubclass(Dog, (Cat, Animal)) # True
issubclass(Poodle, (Cat, Dog)) # TrueTypeError is raised when either argument is not a class.
hash
hash(obj) returns an integer hash value for an object, following Python
semantics:
| Type | Hash rule |
|---|---|
| None | 0 |
| bool | 1 for True, 0 for False |
| int / whole float | the integer value itself |
| other float | derived from the bit pattern |
| str | djb2 algorithm — stable within a process |
| object with __hash__ | dispatches to __hash__() |
| class instance (no __hash__) | stable identity hash (assigned on first call) |
| class with __eq__ but no __hash__ | TypeError (unhashable — Python semantics) |
| list | TypeError: unhashable type: 'list' |
| set | TypeError: unhashable type: 'set' |
| dict | TypeError: unhashable type: 'dict' |
hash(None) # 0
hash(True) # 1
hash(42) # 42
hash(3.0) # 3 (whole float → same as int)
hash('hello') # stable integer
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __hash__(self):
return self.x * 31 + self.y
hash(Point(1, 2)) # 33
# Python semantics: __eq__ without __hash__ → unhashable
class Bar:
def __eq__(self, other):
return True
hash(Bar()) # TypeError: unhashable type: 'Bar'eval and exec
Both eval and exec are supported with Python-compatible signatures.
String literals passed to them are treated as RapydScript source code: the
compiler parses and transpiles the string at compile time, so you write
RapydScript (not raw JavaScript) inside the quotes — just like Python's
eval/exec take Python source strings.
eval(expr[, globals[, locals]])
One argument — the compiled expression is passed to the native JS
eval, giving direct scope access to module-level variables:result = eval("1 + 2") # 3 x = 7 sq = eval("x * x") # 49 (x is in scope)Two or three arguments — uses the
Functionconstructor with explicit variable bindings.localsoverrideglobalswhen both are given:eval("x + y", {"x": 10, "y": 5}) # 15 eval("x", {"x": 1}, {"x": 99}) # 99 (local overrides global)
exec(code[, globals[, locals]])
Executes a RapydScript code string and always returns None, like Python's
exec.
One argument — the compiled code runs via native
eval:exec("print('hi')") # prints hi exec("_x = 42") # _x is discarded after exec returnsTwo or three arguments — uses the
Functionconstructor. Mutable objects (arrays, dicts) passed inglobalsare accessible by reference, so mutations are visible in the caller:log = [] exec("log.append(1 + 2)", {"log": log}) print(log[0]) # 3 def add(a, b): log.append(a + b); exec("fn(10, 7)", {"fn": add, "log": log}) print(log[1]) # 17
Note: Because strings are compiled at compile time, only string literals are transformed — dynamic strings assembled at runtime are passed through unchanged.
exec(code)cannot modify the caller's local variables, matching Python 3 semantics.
vars, locals, and globals
vars(obj)
Returns a dict snapshot of the object's own instance attributes, mirroring
Python's obj.__dict__. Internal RapydScript properties (prefixed ρσ) are
excluded automatically. Mutating the returned dict does not affect the
original object.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
d = vars(p)
print(d['x'], d['y']) # 3 4
print(list(d.keys())) # ['x', 'y']vars() and locals()
Both return an empty dict. JavaScript has no runtime mechanism for
introspecting the local call-frame's variables, so a faithful implementation is
not possible. Use them as placeholders in patterns that require a dict, or
pass explicit dicts where you need named-value lookup.
loc = locals() # {}
v = vars() # {}globals()
Returns a dict snapshot of the JS global object (globalThis / window /
global). Module-level RapydScript variables compiled inside an IIFE or
module wrapper will not appear here; use a shared plain dict for that
pattern instead.
g = globals()
# g contains JS runtime globals such as Math, console, etc.
print('Math' in g) # True (in a browser or Node context)Complex numbers
RapydScript supports Python's complex number type via the complex builtin and
the j/J imaginary literal suffix.
# Imaginary literal suffix
z = 4j # complex(0, 4)
w = 3 + 4j # complex(3, 4) — parsed as 3 + complex(0, 4)
# Constructor
z1 = complex(3, 4) # real=3, imag=4
z2 = complex(5) # real=5, imag=0
z3 = complex() # 0+0j
z4 = complex('2-3j') # string parsing
# Attributes
print(z1.real) # 3
print(z1.imag) # 4
# Methods
print(z1.conjugate()) # (3-4j)
print(abs(z1)) # 5.0 — dispatches __abs__
# Arithmetic (requires overload_operators, which is on by default)
from __python__ import overload_operators
print(z1 + z2) # (8+4j)
print(z1 - z2) # (-2+4j)
print(z1 * z2) # (15+20j)
print(z1 / z2) # (0.6+0.8j)
# Truthiness, repr, isinstance
print(bool(complex(0, 0))) # False
print(repr(z1)) # (3+4j)
print(isinstance(z1, complex)) # TrueThe j/J suffix is handled at the tokenizer level: 4j is parsed into an
AST_Call(complex, 0, 4) node, so it composes naturally with all other
expressions. Mixed expressions like 3 + 4j work without overload_operators
because ρσ_list_add dispatches __radd__ on the right operand.
Attribute-Access Dunders
RapydScript supports the four Python attribute-interception hooks:
__getattr__, __setattr__, __delattr__, and __getattribute__.
When a class defines any of them, instances are automatically wrapped in a
JavaScript Proxy that routes attribute access through the hooks — including
accesses that occur inside __init__.
| Hook | When called |
|---|---|
| __getattr__(self, name) | Fallback — only called when normal lookup finds nothing |
| __setattr__(self, name, value) | Every attribute assignment (including self.x = … in __init__) |
| __delattr__(self, name) | Every del obj.attr |
| __getattribute__(self, name) | Every attribute read (overrides normal lookup) |
To bypass the hooks from within the hook itself (avoiding infinite recursion),
use the object.* bypass functions:
| Python idiom | Compiled form | Effect |
|---|---|---|
| object.__setattr__(self, name, val) | ρσ_object_setattr(self, name, val) | Set attribute directly, bypassing __setattr__ |
| object.__getattribute__(self, name) | ρσ_object_getattr(self, name) | Read attribute directly, bypassing __getattribute__ |
| object.__delattr__(self, name) | ρσ_object_delattr(self, name) | Delete attribute directly, bypassing __delattr__ |
Subclasses automatically inherit proxy wrapping from their parent class — if
Base defines __getattr__, all Child(Base) instances are also Proxy-wrapped.
class Validated:
"""Reject negative values at assignment time."""
def __setattr__(self, name, value):
if jstype(value) is 'number' and value < 0:
raise ValueError(name + ' must be non-negative')
object.__setattr__(self, name, value)
v = Validated()
v.x = 5 # ok
v.x = -1 # ValueError: x must be non-negative
class AttrProxy:
"""Log every attribute read."""
def __init__(self):
object.__setattr__(self, '_log', [])
def __getattribute__(self, name):
self._log.append(name) # self._log goes through __getattribute__ too!
return object.__getattribute__(self, name)Proxy support required — The hooks rely on
Proxy, which is available in all modern browsers and Node.js ≥ 6. In environments that lackProxythe class still works, but the hooks are silently bypassed.
Loops
RapydScript's loops work like Python, not JavaScript. You can't, for example
use for(i=0;i<max;i++) syntax. You can, however, loop through arrays
using 'for ... in' syntax without worrying about the extra irrelevant
attributes regular JavaScript returns.
animals = ['cat', 'dog', 'mouse', 'horse']
for animal in animals:
print('I have a '+animal)If you need to use the index in the loop as well, you can do so by using enumerate():
for index, animal in enumerate(animals):
print("index:"+index, "animal:"+animal)enumerate() supports an optional start argument (default 0):
for index, animal in enumerate(animals, 1):
print(str(index) + '. ' + animal) # 1-based numberingLike in Python, for loops support an else clause that runs only when the loop completes without hitting a break:
for animal in animals:
if animal == 'cat':
print('found a cat')
break
else:
print('no cat found')This is useful for search patterns where you want to take an action only if the searched item was not found.
while loops also support an else clause, which runs when the condition becomes False (i.e. no break was executed):
i = 0
while i < len(items):
if items[i] == target:
print('found at', i)
break
i += 1
else:
print('not found')Like in Python, if you just want the index, you can use range:
for index in range(len(animals)): # or range(animals.length)
print("animal "+index+" is a "+animals[index])When possible, RapydScript will automatically optimize the loop for you into JavaScript's basic syntax, so you're not missing much by not being able to call it directly.
List/Set/Dict Comprehensions
RapydScript also supports comprehensions, using Python syntax. Instead of the following, for example:
myArray = []
for index in range(1,20):
if index*index % 3 == 0:
myArray.append(index*index)You could write this:
myArray = [i*i for i in range(1,20) if i*i%3 == 0]Similarly for set and dict comprehensions:
myDict = {x:x+1 for x in range(20) if x > 2}
mySet = {i*i for i in range(1,20) if i*i%3 == 0}Nested comprehensions (multiple for and if clauses) are also supported,
using the same syntax as Python:
# Flatten a 2-D list
flat = [x for row in matrix for x in row]
# Cartesian product of two ranges
coords = [[i, j] for i in range(3) for j in range(3)]
# Filter across nested loops
evens = [x for row in [[1,2,3],[4,5,6]] for x in row if x % 2 == 0]
# Nested set/dict comprehensions work too
unique_sums = {x + y for x in range(4) for y in range(4)}Any number of for clauses may be combined, each optionally followed by one or
more if conditions.
Builtin iteration functions: any() and all()
RapydScript supports Python's any() and all() built-in functions with
identical semantics. Both work with arrays, strings, iterators, range() objects,
and any other iterable.
any(iterable) returns True if at least one element of the iterable is
truthy, and False if all elements are falsy or the iterable is empty:
any([False, 0, '', 1]) # True
any([False, 0, None]) # False
any([]) # False
any(range(3)) # True (range produces 0, 1, 2 — 1 and 2 are truthy)
any(range(0)) # False (empty range)all(iterable) returns True only if every element is truthy (or the iterable
is empty):
all([1, 2, 3]) # True
all([1, 0, 3]) # False
all([]) # True (vacuously true)
all(range(1, 4)) # True (1, 2, 3 — all truthy)
all(range(0, 3)) # False (range starts at 0, which is falsy)Both functions short-circuit: any() stops as soon as it finds a truthy
element, and all() stops as soon as it finds a falsy element. This makes
them efficient even with large or lazy iterables.
They work naturally with list comprehensions for expressive one-liners:
nums = [2, 4, 6, 8]
all([x > 0 for x in nums]) # True — all positive
any([x > 5 for x in nums]) # True — some greater than 5
all([x > 5 for x in nums]) # False — not all greater than 5Both any() and all() compile to plain JavaScript function calls and are
always available without any import.
Strings
For reasons of compatibility with external JavaScript and performance,
RapydScript does not make any changes to the native JavaScript string type.
However, all the useful Python string methods are available on the builtin
str object. This is analogous to how the functions are available in the
string module in Python 2.x. For example,
str.strip(' a ') == 'a'
str.split('a b') == ['a', 'b']
str.format('{0:02d} {n}', 1, n=2) == '01 2'
...The format(value[, spec]) builtin is also supported. It applies the Python
format-spec mini-language to a single value — the same mini-language that
follows : in f-strings and str.format() fields:
format(42, '08b') # '00101010' — zero-padded binary
format(3.14159, '.2f') # '3.14' — fixed-point
format('hi', '>10') # ' hi' — right-aligned in 10-char field
format(42) # '42' — no spec: same as str(42)Objects with a __format__ method are dispatched to it in all three contexts
— format(obj, spec), str.format('{:spec}', obj), and f'{obj:spec}' —
matching Python's protocol exactly. Every user-defined class automatically
gets a default __format__ that returns str(self) for an empty spec and
raises TypeError for any other spec, just like object.__format__ in
Python:
class Money:
def __init__(self, amount):
self.amount = amount
def __str__(self):
return str(self.amount)
def __format__(self, spec):
if spec == 'usd':
return '$' + str(self.amount)
return format(self.amount, spec) # delegate numeric specs
m = Money(42)
format(m, 'usd') # '$42'
str.format('{:usd}', m) # '$42'
f'{m:usd}' # '$42'
f'{m:.2f}' # '42.00'The !r, !s, and !a conversion flags apply repr()/str()/repr() to the
value before formatting, bypassing __format__ (same as Python).
String predicate methods are also available:
str.isalpha('abc') # True — all alphabetic
str.isdigit('123') # True — all digits
str.isalnum('abc123') # True — alphanumeric
str.isspace(' ') # True — all whitespace
str.isupper('ABC') # True
str.islower('abc') # True
str.isidentifier('my_var') # True — valid Python identifierPython 3.9 prefix/suffix removal:
str.removeprefix('HelloWorld', 'Hello') # 'World'
str.removesuffix('HelloWorld', 'World') # 'Hello'Case-folding for locale-insensitive lowercase comparison:
str.casefold('ÄÖÜ') == str.casefold('äöü') # True (maps to lowercase)Tab expansion:
str.expandtabs('a\tb', 4) # 'a b' — expand to next 4-space tab stop
str.expandtabs('\t\t', 8) # ' ' — two full tab stops
str.expandtabs('ab\tc', 4) # 'ab c' — only 2 spaces needed to reach next stopThe optional tabsize argument defaults to 8, matching Python's default. A tabsize of 0 removes all tab characters. Newline (\n) and carriage-return (\r) characters reset the column counter, so each line is expanded independently.
However, if you want to make the python string methods available on string objects, there is a convenience method in the standard library to do so. Use the following code:
from pythonize import strings
strings()After you call the strings() function, all python string methods will be
available on string objects, just as in python. The only caveat is that two
methods: split() and replace() are left as the native JavaScript versions,
as their behavior is not compatible with that of the python versions. You can
control which methods are not copied to the JavaScript String object by passing
their names to the strings() function, like this:
strings('split', 'replace', 'find', ...)
# or
strings(None) # no methods are excludedOne thing to keep in mind is that in JavaScript string are UTF-16, so they behave like strings in narrow builds of Python 2.x. This means that non-BMP unicode characters are represented as surrogate pairs. RapydScript includes some functions to make dealing with non-BMP unicode characters easier:
str.uchrs(string, [with_positions])-- iterate over unicode characters in string, so, for example:list(str.uchrs('s🐱a')) == ['s', "🐱", 'a']You can also get positions of individual characters:
list(str.uchrs('s🐱a', True)) == [[0, 's'], [1, "🐱"], [3, 'a']]Note that any broken surrogate pairs in the underlying string are returned as the unicode replacement character U+FFFD
str.uslice(string, [start, [stop]])-- get a slice based on unicode character positions, for example:str.uslice('s🐱a', 2') == 'a' # even though a is at index 3 in the native string objectstr.ulen(string)-- return the number of unicode characters in the string
The Existential Operator
One of the annoying warts of JavaScript is that there are two "null-like"
values: undefined and null. So if you want to test if a variable is not
null you often have to write a lengthy expression that looks like
(var !== undefined and var !== None)Simply doing bool(var) will not work because zero and empty strings are also
False.
Similarly, if you need to access a chain of properties/keys and dont want a
TypeError to be raised, if one of them is undefined/null then you have
to do something like:
if a and a.b and a.b.c:
ans = a.b.c()
else:
ans = undefinedTo ease these irritations, RapydScript borrows the Existential operator from CoffeeScript. This can be used to test if a variable is null-like, with a single character, like this:
yes = True if no? else False
# Which, without the ? operator becomes
yes = True if no is not undefined and no is not None else FalseWhen it comes to long chains, the ? operator will return the expected value
if all parts of the chain are ok, but cause the entire chaning to result in
undefined if any of its links are null-like. For example:
ans = a?.b?[1]?()
# Which, without the ? operator becomes
ans = undefined
if a is not undefined and a is not None and a.b is not undefined and a.b is not None and jstype(a.b[1]) is 'function':
ans = a.b[1]()Finally, you can also use the existential operator as shorthand for the conditional ternary operator, like this:
a = b ? c
# is the same as
a = c if (b is undefined or b is None) else bWalrus Operator
RapydScript supports the walrus (assignment expression) operator := from
Python 3.8 (PEP 572). It assigns a value and returns it as an expression,
allowing you to avoid repeating a computation:
# assign and test in a single expression
if m := re.match(r'\d+', line):
print(m.group())
# drain an iterable in a while loop
while chunk := file.read(8192):
process(chunk)
# filter and capture in a comprehension
results = [y for x in data if (y := transform(x)) is not None]The walrus operator binds to the nearest enclosing function or module scope (not the comprehension scope), matching Python semantics.
Ellipsis Literal
RapydScript supports the Python ... (Ellipsis) literal. It compiles to a
frozen singleton object ρσ_Ellipsis and is also accessible as Ellipsis
(the global name), matching Python behaviour.
Common uses:
# As a placeholder body (like pass)
def stub():
...
# As a sentinel / marker value
def process(data, mask=...):
if mask is ...:
