Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ jobs:
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '24.19.0'
- name: Require Node conformance runner
run: node --version
- name: Install build tools
run: python -m pip install --upgrade build twine
- name: Build sdist and wheel from sdist
run: python -m build
- name: Check distribution metadata
run: python -m twine check --strict dist/*
- name: Install wheel with published dependencies
run: python -m pip install --find-links dist Python-JavaScript==0.1.0
run: python scripts/install_distribution.py wheel
- name: Test installed wheel
run: python -m unittest discover -s tests -v
- name: Install source distribution with published dependencies
run: python scripts/install_distribution.py sdist
- name: Test installed source distribution
run: python -m unittest discover -s tests -v
80 changes: 80 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Release

on:
release:
types: [published]

permissions:
contents: read

concurrency:
group: pypi-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '24.19.0'
- name: Require Node conformance runner
run: node --version
- name: Verify release tag matches package version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
python - <<'PYTHON'
import os
import tomllib
from pathlib import Path

project = tomllib.loads(Path("pyproject.toml").read_text())["project"]
expected = "v" + project["version"]
if os.environ["RELEASE_TAG"] != expected:
raise SystemExit(f"Release tag must be {expected}")
PYTHON
- name: Install build tools
run: python -m pip install --upgrade build twine
- name: Build sdist and wheel from the sdist
run: python -m build
- name: Check distribution metadata
run: python -m twine check --strict dist/*
- name: Install the wheel
run: python scripts/install_distribution.py wheel
- name: Test the installed wheel
run: python -m unittest discover -s tests -v
- name: Install and test source distribution
run: |
python scripts/install_distribution.py sdist
python -m unittest discover -s tests -v
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: distributions
path: dist/
if-no-files-found: error
retention-days: 7

publish:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: pypi
url: https://pypi.org/project/Python-JavaScript/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: distributions
path: dist/
- name: Publish to PyPI using Trusted Publishing
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

## 0.1.0

- Introduce the case-sensitive `JavaScript` namespace with Object, sparse Array,
UTF-16 String methods, Number, Boolean, selected Math functions and JSON.
- Add undefined, truthiness, conversion, equality and parsing helpers.
- Depend on standalone JavaScript-Fetch-API 0.2 and JavaScript-JSON 0.1; expose
Fetch and Promise without duplicating their implementations.
- Document the supported compatibility subset and future translation direction.
100 changes: 97 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,99 @@
# Python-JavaScript

JavaScript-style objects and standard-library methods in Python, using the
case-sensitive `JavaScript` namespace. Development is tracked in this
repository's issues and the organization Project.
JavaScript-style objects, methods and functions for Python 3.14+, using the
case-sensitive `JavaScript` namespace. An alpha compatibility layer with a
documented subset of ECMAScript behavior.

```sh
python -m pip install Python-JavaScript==0.1.0
```

```python
from JavaScript import Array, JSON, Math, Object, String, undefined

people = JSON.parse('[{"name":"Ada","score":8},{"name":"Lin","score":6}]')
names = people.filter(lambda person: person.score >= 7).map(
lambda person: person.name.toUpperCase()
)
assert names.join(", ") == "ADA"

profile = Object({"name": String("Ada"), "scores": Array.of(8, 6)})
assert profile.missing is undefined
assert profile.scores.reduce(lambda total, score: total + score, 0) == 14
assert Math.round(2.5) == 3
assert JSON.stringify(profile) == '{"name":"Ada","scores":[8,6]}'
```

Explicit `Object`, `Array`, `String` and `Number` values expose JavaScript names
and behavior. Ordinary Python literals and operators retain Python semantics.
Objects and arrays compare by identity and remain truthy when empty. Use
`Boolean(value)` or `truthy(value)` when testing JavaScript truthiness of values
that might include Python containers.

## Standalone packages

Installation includes two independently useful dependencies:

| Distribution | Import | Purpose |
| --- | --- | --- |
| `JavaScript-Fetch-API>=0.2.0,<0.3.0` | `fetch_api` | Standard-library HTTP Fetch and Promise APIs |
| `JavaScript-JSON>=0.1.0,<0.2.0` | `JSON` | Python-friendly JSON parsing, serialization and dot traversal |

```python
import asyncio
from fetch_api import fetch
from JavaScript import JSON

async def main():
response = await fetch("https://example.com/api/people")
if not response.ok:
raise RuntimeError(f"HTTP {response.status}")
people = JSON.parse(await response.text())
return people

# Run against your own JSON endpoint:
# people = asyncio.run(main())
```

`JavaScript.fetch`, `Promise`, `Headers`, `Request`, `Response` and `FetchError`
are the same objects exported by `fetch_api`. Fetch 0.2.0 changes the old
`from javascript import fetch` spelling to `from fetch_api import fetch`.

`response.json()` retains Fetch's standalone `JSONObject`/list result. Use
`JavaScript.JSON.parse(await response.text())` for runtime Object/Array/String
values. Standalone `import JSON` preserves Python integers and containers;
`from JavaScript import JSON` uses binary64 numbers and JavaScript serialization
rules for its supported subset.

## Supported surface

Core data objects and sparse arrays; UTF-16 String methods; Number predicates and
prefix parsers; Boolean conversion; selected Math functions; JSON; and the
standalone Fetch/Promise APIs. See the complete
[compatibility matrix](docs/compatibility.md) for method lists and limits.

Python keywords require `Array.from_`, `Object.is_`, and `Promise.finally_`.
`None` represents `null`; `undefined` is a separate singleton. Array bracket
indices follow JavaScript rules: `values[-1]` is a named property, and
`values.at(-1)` accesses the last element. `Array(3)` creates three holes;
`Array.of(3)` creates one element containing 3.

The longer-term goal is a binding-aware Python-to-JavaScript translator. v0.1.0
provides the runtime foundation; it does not translate source or execute
JavaScript. Prototypes, descriptors, Date, RegExp, Map/Set, typed arrays, browser
globals and other unlisted APIs remain future work. Development is tracked in
the [issues](https://github.com/outside-labs/Python-JavaScript/issues) and
[Project](https://github.com/orgs/outside-labs/projects/11).

## Development

```sh
uv sync
uv run python -m unittest discover -s tests -v
uv build
```

Node.js 24 runs the native ECMAScript differential fixture. That test is skipped
when Node is absent locally; CI and release builds require it. Runtime code
needs only Python and the two declared companions. Tests make no external
network requests.
35 changes: 35 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Publishing Python-JavaScript

The distribution name is `Python-JavaScript`; the Python import is `JavaScript`.
Requires Python 3.14 or later.

## Trusted Publisher

The PyPI publisher must match these values exactly:

| Field | Value |
| --- | --- |
| PyPI project | `Python-JavaScript` |
| GitHub owner | `outside-labs` |
| Repository | `Python-JavaScript` |
| Workflow | `release.yaml` |
| Environment | `pypi` |

The GitHub environment is named `pypi`. Publishing uses OpenID Connect; no API
token belongs in the repository or workflow.

## Release

1. Merge the passing task PRs and confirm final main CI on all three platforms.
2. Confirm both declared companions are available on PyPI. Verify clean wheel
and sdist installs and review the packaged file list.
3. Set the package version and matching changelog. Create and publish the GitHub
release with tag `v<version>` targeting the verified main commit.
4. Monitor Release: tag/version validation, source/wheel build, strict metadata,
installed-wheel and installed-sdist tests, then publication via `pypi`.
5. Verify a new environment can install `Python-JavaScript==<version>` from PyPI,
run the examples/tests, and compare published SHA256 hashes with build artifacts.

Publishing a GitHub release triggers upload; a tag push alone does not. PyPI
versions are immutable. Repository visibility stays private; published artifacts
include package source. No open-source license has been selected for this release.
119 changes: 119 additions & 0 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# v0.1.0 compatibility

This is a selected ECMAScript-compatible runtime API implemented in Python.
Method names and the tested behaviors below follow the
[ECMAScript specification](https://tc39.es/ecma262/). It is not a complete language
implementation. The native Node.js fixture checks the adopted subset, rather
than asserting general Test262 conformance.

## Supported API

| Surface | Implemented members |
| --- | --- |
| Values | `undefined`, `null`, `NaN`, `Infinity`, `typeof`, `truthy`, `strictEqual`, global `isNaN`, global `isFinite` |
| `Object` | Mapping/empty construction, dot/bracket data properties, deletion, missing `undefined`, `keys`, `values`, `entries`, `assign`, `fromEntries`, `hasOwn`, `is_`, `toString` |
| `Array` | Numeric-length/element construction, `of`, iterable `from_`, `isArray`, `length`, bracket access/deletion, `at`, `push`, `pop`, `shift`, `unshift`, `slice`, `splice`, `concat`, `join`, `toString`, `includes`, `indexOf`, `lastIndexOf` |
| Array callbacks | `map`, `filter`, `forEach`, `some`, `every`, `find`, `findIndex`, `findLast`, `findLastIndex`, `reduce`, `reduceRight` |
| Array mutation/iteration | `reverse`, `fill`, `sort`, `keys`, `values`, `entries`, Python iteration |
| `String` | Conversion, `length`, bracket code units, `at`, `charAt`, `charCodeAt`, `codePointAt`, `slice`, `substring`, `indexOf`, `lastIndexOf`, `includes`, `startsWith`, `endsWith`, `split`, `concat`, `repeat`, `trim`, `trimStart`, `trimEnd`, `toLowerCase`, `toUpperCase`, `toString`, `valueOf`, `fromCharCode`, `fromCodePoint` |
| `Number` | Conversion, `isNaN`, `isFinite`, `isInteger`, `isSafeInteger`, `parseInt`, `parseFloat`, decimal `toString`, `valueOf`, `EPSILON`, `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `NaN`, `POSITIVE_INFINITY`, `NEGATIVE_INFINITY` |
| `Boolean` | Primitive conversion returning a Python bool |
| `Math` | `abs`, `floor`, `ceil`, `trunc`, `round`, `sign`, `min`, `max`, `sqrt`, `pow`, `random`; `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT1_2`, `SQRT2` |
| `JSON` | `parse(text)`, `stringify(value, replacer=undefined, space=undefined)`, `JSONDecodeError` |
| Global parsing | `parseInt`, `parseFloat` |
| Companions | `fetch`, `FetchError`, `Headers`, `Request`, `Response`, `Promise`, re-exported unchanged from `fetch_api` |

## Data model and Python boundaries

- `Object` stores own enumerable writable data properties. Keys convert to
strings. Array-index keys enumerate numerically before other keys in insertion
order. Deleting and re-adding a non-index key moves it to the end.
- `Object()`/`Object(None)` create empty objects; `Object(existing)` returns the
existing Object. Other construction accepts a Python mapping. Primitive
boxing and prototype/descriptor operations are unsupported.
- Object-to-primitive conversion uses the built-in data-object representation.
Custom `valueOf`/`toString` conversion hooks and Symbol.toPrimitive are not
implemented.
- Objects preserve supplied child values; passing a nested Python dict/list
does not recursively convert it. JSON parsing creates runtime containers.
- Own Object keys shadow ordinary Python attributes. Use `Object.keys(obj)`
for static methods and brackets for special names such as `__class__`.
Missing ordinary attributes return `undefined`; Python `hasattr` therefore
cannot test property presence. Use `Object.hasOwn` or `key in obj`.
- Arrays store holes separately from explicit undefined values. Length changes
truncate or expand sparsely. Callback methods skip holes where ECMAScript does;
find methods and includes observe holes as undefined. `Object.hasOwn(array, i)`
distinguishes them. `length` is an own, non-enumerable property.
- Use bracket notation for named Array properties; arbitrary dot property
assignment and overriding Array methods are outside this subset. Python `in`
tests property presence, matching JavaScript `in`; use `includes` for values.
- `Array.from_` accepts Python iterables, including runtime arrays and strings.
JavaScript array-like objects, custom iterators/Symbol protocols, `thisArg`,
subclass species and `Symbol.isConcatSpreadable` are not implemented. `concat`
spreads runtime Array values; ordinary Python lists remain single elements.
- Callbacks receive JavaScript's arguments, truncated to their inspectable Python
positional arity. Zero-argument and variadic callbacks work. Uninspectable
callables receive all arguments; wrap them in a lambda if needed. Keyword-only
callback parameters and JavaScript `this` binding are unsupported. Exceptions
propagate once without retrying the callback.
- `sort` is stable and uses UTF-16 lexicographic order by default. Undefined
elements follow defined elements; holes follow undefined. Comparators may
return a number; mutating the sorted array from its comparator is unsupported.
- String methods and brackets index UTF-16 code units. String iteration visits
code points. Python `len(string)` and inherited str methods/operators retain
Python semantics; use `.length` for JavaScript length. Case conversion uses
Python's Unicode tables, which can differ from a JavaScript engine's version.
- `Number` is a float subclass with binary64 conversion. Plain Python integers,
arithmetic, division, modulo, comparisons, `and`/`or`, `==`, container literals
and inherited methods retain Python behavior. Use explicit helpers for
JavaScript semantics. No operator rewriting or automatic primitive boxing is
provided. `Boolean` does not create boxed Boolean objects.
- Methods can return native Python primitives, including Array `join` and Math
results. Wrap such results with `String` or `Number` to call their JavaScript
methods; Python does not automatically box primitive values.
- Decimal number strings use Python's shortest float representation adjusted to
JavaScript exponent/fixed-format thresholds. Tested ordinary values agree;
rare shortest-decimal tie choices may differ between Python and ECMAScript.
Other number radices and `toFixed`/`toPrecision` are not implemented.
- Errors use Python `TypeError`, `ValueError` and `JSONDecodeError`; no full
ECMAScript Error hierarchy or matching message text is promised. Invalid array
lengths, repeat counts and code points raise ValueError. Invalid JSON numeric
constants raise ValueError. Unsupported overloads raise TypeError or
NotImplementedError.

## JSON boundaries

`JavaScript.JSON.parse` creates Object, Array and String values, and binary64
Python floats for numbers. Negative zero and integer rounding follow the number
model. It uses the standard-library decoder's numeric hooks because standalone
`JSON.parse` intentionally preserves Python integers. The companion's serializer
provides string escaping; its public decoder exception type is shared.

`stringify` emits compact JSON by default, orders object keys, writes non-finite
numbers as null, omits undefined/callable object properties, and writes them as
null in arrays. Top-level undefined/callables return undefined. Shared references
are allowed; cycles raise TypeError. A numeric/string `space` is capped at ten
characters. Lone surrogates are escaped. Plain Python mappings/lists/tuples are
accepted as a convenience.

Revivers, replacer functions/property lists, `toJSON` hooks, symbols and BigInt
are unsupported. A non-null replacer or callable own toJSON hook raises
NotImplementedError. Byte input is a standalone JSON extension and is rejected
by the runtime facade. The facade does not change standalone `import JSON` or
Fetch's `response.json()` result types.

## Fetch and Promise boundaries

The [standalone Fetch documentation](https://github.com/outside-labs/JavaScript-Fetch-API)
is the complete contract. Core HTTP(S) requests, buffered bodies, headers,
redirects, promises and await work. Browser policies, streaming, abort, cookies
and browser globals are not provided. Promise callbacks use a background Python
thread, not JavaScript microtask scheduling. Rejections use Python exceptions;
handlers return values or Promise objects, rather than raw Python coroutines.

## Future translation

The runtime deliberately keeps recognizable call shapes, but a Python program
using it is not automatically safe to translate. The future translator must
resolve bindings and validate an approved Python AST subset. It must diagnose
unsupported operators and Python-only constructs before emitting JavaScript.
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@ build-backend = "uv_build"

[tool.uv.build-backend]
module-name = "JavaScript"
source-include = [
"tests/test_*.py",
"tests/fixtures/conformance.mjs",
"docs/compatibility.md",
"scripts/install_distribution.py",
"CHANGELOG.md",
"RELEASING.md",
]
Loading
Loading