Skip to content

Commit b4d70df

Browse files
authored
test: verify conformance and prepare v0.1.0 release (#19)
* feat: establish JavaScript values and packaging * feat: add Object and sparse Array methods * feat: add primitive methods and JSON adapters * test: verify conformance and prepare v0.1.0 release * docs: clarify primitive conversion boundaries
1 parent 1a0a86e commit b4d70df

11 files changed

Lines changed: 644 additions & 4 deletions

File tree

‎.github/workflows/ci.yaml‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,22 @@ jobs:
2424
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
2525
with:
2626
python-version: '3.14'
27+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
28+
with:
29+
node-version: '24.19.0'
30+
- name: Require Node conformance runner
31+
run: node --version
2732
- name: Install build tools
2833
run: python -m pip install --upgrade build twine
2934
- name: Build sdist and wheel from sdist
3035
run: python -m build
3136
- name: Check distribution metadata
3237
run: python -m twine check --strict dist/*
3338
- name: Install wheel with published dependencies
34-
run: python -m pip install --find-links dist Python-JavaScript==0.1.0
39+
run: python scripts/install_distribution.py wheel
3540
- name: Test installed wheel
3641
run: python -m unittest discover -s tests -v
42+
- name: Install source distribution with published dependencies
43+
run: python scripts/install_distribution.py sdist
44+
- name: Test installed source distribution
45+
run: python -m unittest discover -s tests -v

‎.github/workflows/release.yaml‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: pypi-${{ github.ref }}
12+
cancel-in-progress: false
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 10
18+
steps:
19+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20+
with:
21+
persist-credentials: false
22+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
23+
with:
24+
python-version: '3.14'
25+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
26+
with:
27+
node-version: '24.19.0'
28+
- name: Require Node conformance runner
29+
run: node --version
30+
- name: Verify release tag matches package version
31+
env:
32+
RELEASE_TAG: ${{ github.event.release.tag_name }}
33+
run: |
34+
python - <<'PYTHON'
35+
import os
36+
import tomllib
37+
from pathlib import Path
38+
39+
project = tomllib.loads(Path("pyproject.toml").read_text())["project"]
40+
expected = "v" + project["version"]
41+
if os.environ["RELEASE_TAG"] != expected:
42+
raise SystemExit(f"Release tag must be {expected}")
43+
PYTHON
44+
- name: Install build tools
45+
run: python -m pip install --upgrade build twine
46+
- name: Build sdist and wheel from the sdist
47+
run: python -m build
48+
- name: Check distribution metadata
49+
run: python -m twine check --strict dist/*
50+
- name: Install the wheel
51+
run: python scripts/install_distribution.py wheel
52+
- name: Test the installed wheel
53+
run: python -m unittest discover -s tests -v
54+
- name: Install and test source distribution
55+
run: |
56+
python scripts/install_distribution.py sdist
57+
python -m unittest discover -s tests -v
58+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
59+
with:
60+
name: distributions
61+
path: dist/
62+
if-no-files-found: error
63+
retention-days: 7
64+
65+
publish:
66+
needs: build
67+
runs-on: ubuntu-latest
68+
timeout-minutes: 10
69+
environment:
70+
name: pypi
71+
url: https://pypi.org/project/Python-JavaScript/
72+
permissions:
73+
id-token: write
74+
steps:
75+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
76+
with:
77+
name: distributions
78+
path: dist/
79+
- name: Publish to PyPI using Trusted Publishing
80+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
3+
## 0.1.0
4+
5+
- Introduce the case-sensitive `JavaScript` namespace with Object, sparse Array,
6+
UTF-16 String methods, Number, Boolean, selected Math functions and JSON.
7+
- Add undefined, truthiness, conversion, equality and parsing helpers.
8+
- Depend on standalone JavaScript-Fetch-API 0.2 and JavaScript-JSON 0.1; expose
9+
Fetch and Promise without duplicating their implementations.
10+
- Document the supported compatibility subset and future translation direction.

‎README.md‎

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,99 @@
11
# Python-JavaScript
22

3-
JavaScript-style objects and standard-library methods in Python, using the
4-
case-sensitive `JavaScript` namespace. Development is tracked in this
5-
repository's issues and the organization Project.
3+
JavaScript-style objects, methods and functions for Python 3.14+, using the
4+
case-sensitive `JavaScript` namespace. An alpha compatibility layer with a
5+
documented subset of ECMAScript behavior.
6+
7+
```sh
8+
python -m pip install Python-JavaScript==0.1.0
9+
```
10+
11+
```python
12+
from JavaScript import Array, JSON, Math, Object, String, undefined
13+
14+
people = JSON.parse('[{"name":"Ada","score":8},{"name":"Lin","score":6}]')
15+
names = people.filter(lambda person: person.score >= 7).map(
16+
lambda person: person.name.toUpperCase()
17+
)
18+
assert names.join(", ") == "ADA"
19+
20+
profile = Object({"name": String("Ada"), "scores": Array.of(8, 6)})
21+
assert profile.missing is undefined
22+
assert profile.scores.reduce(lambda total, score: total + score, 0) == 14
23+
assert Math.round(2.5) == 3
24+
assert JSON.stringify(profile) == '{"name":"Ada","scores":[8,6]}'
25+
```
26+
27+
Explicit `Object`, `Array`, `String` and `Number` values expose JavaScript names
28+
and behavior. Ordinary Python literals and operators retain Python semantics.
29+
Objects and arrays compare by identity and remain truthy when empty. Use
30+
`Boolean(value)` or `truthy(value)` when testing JavaScript truthiness of values
31+
that might include Python containers.
32+
33+
## Standalone packages
34+
35+
Installation includes two independently useful dependencies:
36+
37+
| Distribution | Import | Purpose |
38+
| --- | --- | --- |
39+
| `JavaScript-Fetch-API>=0.2.0,<0.3.0` | `fetch_api` | Standard-library HTTP Fetch and Promise APIs |
40+
| `JavaScript-JSON>=0.1.0,<0.2.0` | `JSON` | Python-friendly JSON parsing, serialization and dot traversal |
41+
42+
```python
43+
import asyncio
44+
from fetch_api import fetch
45+
from JavaScript import JSON
46+
47+
async def main():
48+
response = await fetch("https://example.com/api/people")
49+
if not response.ok:
50+
raise RuntimeError(f"HTTP {response.status}")
51+
people = JSON.parse(await response.text())
52+
return people
53+
54+
# Run against your own JSON endpoint:
55+
# people = asyncio.run(main())
56+
```
57+
58+
`JavaScript.fetch`, `Promise`, `Headers`, `Request`, `Response` and `FetchError`
59+
are the same objects exported by `fetch_api`. Fetch 0.2.0 changes the old
60+
`from javascript import fetch` spelling to `from fetch_api import fetch`.
61+
62+
`response.json()` retains Fetch's standalone `JSONObject`/list result. Use
63+
`JavaScript.JSON.parse(await response.text())` for runtime Object/Array/String
64+
values. Standalone `import JSON` preserves Python integers and containers;
65+
`from JavaScript import JSON` uses binary64 numbers and JavaScript serialization
66+
rules for its supported subset.
67+
68+
## Supported surface
69+
70+
Core data objects and sparse arrays; UTF-16 String methods; Number predicates and
71+
prefix parsers; Boolean conversion; selected Math functions; JSON; and the
72+
standalone Fetch/Promise APIs. See the complete
73+
[compatibility matrix](docs/compatibility.md) for method lists and limits.
74+
75+
Python keywords require `Array.from_`, `Object.is_`, and `Promise.finally_`.
76+
`None` represents `null`; `undefined` is a separate singleton. Array bracket
77+
indices follow JavaScript rules: `values[-1]` is a named property, and
78+
`values.at(-1)` accesses the last element. `Array(3)` creates three holes;
79+
`Array.of(3)` creates one element containing 3.
80+
81+
The longer-term goal is a binding-aware Python-to-JavaScript translator. v0.1.0
82+
provides the runtime foundation; it does not translate source or execute
83+
JavaScript. Prototypes, descriptors, Date, RegExp, Map/Set, typed arrays, browser
84+
globals and other unlisted APIs remain future work. Development is tracked in
85+
the [issues](https://github.com/outside-labs/Python-JavaScript/issues) and
86+
[Project](https://github.com/orgs/outside-labs/projects/11).
87+
88+
## Development
89+
90+
```sh
91+
uv sync
92+
uv run python -m unittest discover -s tests -v
93+
uv build
94+
```
95+
96+
Node.js 24 runs the native ECMAScript differential fixture. That test is skipped
97+
when Node is absent locally; CI and release builds require it. Runtime code
98+
needs only Python and the two declared companions. Tests make no external
99+
network requests.

‎RELEASING.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Publishing Python-JavaScript
2+
3+
The distribution name is `Python-JavaScript`; the Python import is `JavaScript`.
4+
Requires Python 3.14 or later.
5+
6+
## Trusted Publisher
7+
8+
The PyPI publisher must match these values exactly:
9+
10+
| Field | Value |
11+
| --- | --- |
12+
| PyPI project | `Python-JavaScript` |
13+
| GitHub owner | `outside-labs` |
14+
| Repository | `Python-JavaScript` |
15+
| Workflow | `release.yaml` |
16+
| Environment | `pypi` |
17+
18+
The GitHub environment is named `pypi`. Publishing uses OpenID Connect; no API
19+
token belongs in the repository or workflow.
20+
21+
## Release
22+
23+
1. Merge the passing task PRs and confirm final main CI on all three platforms.
24+
2. Confirm both declared companions are available on PyPI. Verify clean wheel
25+
and sdist installs and review the packaged file list.
26+
3. Set the package version and matching changelog. Create and publish the GitHub
27+
release with tag `v<version>` targeting the verified main commit.
28+
4. Monitor Release: tag/version validation, source/wheel build, strict metadata,
29+
installed-wheel and installed-sdist tests, then publication via `pypi`.
30+
5. Verify a new environment can install `Python-JavaScript==<version>` from PyPI,
31+
run the examples/tests, and compare published SHA256 hashes with build artifacts.
32+
33+
Publishing a GitHub release triggers upload; a tag push alone does not. PyPI
34+
versions are immutable. Repository visibility stays private; published artifacts
35+
include package source. No open-source license has been selected for this release.

‎docs/compatibility.md‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# v0.1.0 compatibility
2+
3+
This is a selected ECMAScript-compatible runtime API implemented in Python.
4+
Method names and the tested behaviors below follow the
5+
[ECMAScript specification](https://tc39.es/ecma262/). It is not a complete language
6+
implementation. The native Node.js fixture checks the adopted subset, rather
7+
than asserting general Test262 conformance.
8+
9+
## Supported API
10+
11+
| Surface | Implemented members |
12+
| --- | --- |
13+
| Values | `undefined`, `null`, `NaN`, `Infinity`, `typeof`, `truthy`, `strictEqual`, global `isNaN`, global `isFinite` |
14+
| `Object` | Mapping/empty construction, dot/bracket data properties, deletion, missing `undefined`, `keys`, `values`, `entries`, `assign`, `fromEntries`, `hasOwn`, `is_`, `toString` |
15+
| `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` |
16+
| Array callbacks | `map`, `filter`, `forEach`, `some`, `every`, `find`, `findIndex`, `findLast`, `findLastIndex`, `reduce`, `reduceRight` |
17+
| Array mutation/iteration | `reverse`, `fill`, `sort`, `keys`, `values`, `entries`, Python iteration |
18+
| `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` |
19+
| `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` |
20+
| `Boolean` | Primitive conversion returning a Python bool |
21+
| `Math` | `abs`, `floor`, `ceil`, `trunc`, `round`, `sign`, `min`, `max`, `sqrt`, `pow`, `random`; `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT1_2`, `SQRT2` |
22+
| `JSON` | `parse(text)`, `stringify(value, replacer=undefined, space=undefined)`, `JSONDecodeError` |
23+
| Global parsing | `parseInt`, `parseFloat` |
24+
| Companions | `fetch`, `FetchError`, `Headers`, `Request`, `Response`, `Promise`, re-exported unchanged from `fetch_api` |
25+
26+
## Data model and Python boundaries
27+
28+
- `Object` stores own enumerable writable data properties. Keys convert to
29+
strings. Array-index keys enumerate numerically before other keys in insertion
30+
order. Deleting and re-adding a non-index key moves it to the end.
31+
- `Object()`/`Object(None)` create empty objects; `Object(existing)` returns the
32+
existing Object. Other construction accepts a Python mapping. Primitive
33+
boxing and prototype/descriptor operations are unsupported.
34+
- Object-to-primitive conversion uses the built-in data-object representation.
35+
Custom `valueOf`/`toString` conversion hooks and Symbol.toPrimitive are not
36+
implemented.
37+
- Objects preserve supplied child values; passing a nested Python dict/list
38+
does not recursively convert it. JSON parsing creates runtime containers.
39+
- Own Object keys shadow ordinary Python attributes. Use `Object.keys(obj)`
40+
for static methods and brackets for special names such as `__class__`.
41+
Missing ordinary attributes return `undefined`; Python `hasattr` therefore
42+
cannot test property presence. Use `Object.hasOwn` or `key in obj`.
43+
- Arrays store holes separately from explicit undefined values. Length changes
44+
truncate or expand sparsely. Callback methods skip holes where ECMAScript does;
45+
find methods and includes observe holes as undefined. `Object.hasOwn(array, i)`
46+
distinguishes them. `length` is an own, non-enumerable property.
47+
- Use bracket notation for named Array properties; arbitrary dot property
48+
assignment and overriding Array methods are outside this subset. Python `in`
49+
tests property presence, matching JavaScript `in`; use `includes` for values.
50+
- `Array.from_` accepts Python iterables, including runtime arrays and strings.
51+
JavaScript array-like objects, custom iterators/Symbol protocols, `thisArg`,
52+
subclass species and `Symbol.isConcatSpreadable` are not implemented. `concat`
53+
spreads runtime Array values; ordinary Python lists remain single elements.
54+
- Callbacks receive JavaScript's arguments, truncated to their inspectable Python
55+
positional arity. Zero-argument and variadic callbacks work. Uninspectable
56+
callables receive all arguments; wrap them in a lambda if needed. Keyword-only
57+
callback parameters and JavaScript `this` binding are unsupported. Exceptions
58+
propagate once without retrying the callback.
59+
- `sort` is stable and uses UTF-16 lexicographic order by default. Undefined
60+
elements follow defined elements; holes follow undefined. Comparators may
61+
return a number; mutating the sorted array from its comparator is unsupported.
62+
- String methods and brackets index UTF-16 code units. String iteration visits
63+
code points. Python `len(string)` and inherited str methods/operators retain
64+
Python semantics; use `.length` for JavaScript length. Case conversion uses
65+
Python's Unicode tables, which can differ from a JavaScript engine's version.
66+
- `Number` is a float subclass with binary64 conversion. Plain Python integers,
67+
arithmetic, division, modulo, comparisons, `and`/`or`, `==`, container literals
68+
and inherited methods retain Python behavior. Use explicit helpers for
69+
JavaScript semantics. No operator rewriting or automatic primitive boxing is
70+
provided. `Boolean` does not create boxed Boolean objects.
71+
- Methods can return native Python primitives, including Array `join` and Math
72+
results. Wrap such results with `String` or `Number` to call their JavaScript
73+
methods; Python does not automatically box primitive values.
74+
- Decimal number strings use Python's shortest float representation adjusted to
75+
JavaScript exponent/fixed-format thresholds. Tested ordinary values agree;
76+
rare shortest-decimal tie choices may differ between Python and ECMAScript.
77+
Other number radices and `toFixed`/`toPrecision` are not implemented.
78+
- Errors use Python `TypeError`, `ValueError` and `JSONDecodeError`; no full
79+
ECMAScript Error hierarchy or matching message text is promised. Invalid array
80+
lengths, repeat counts and code points raise ValueError. Invalid JSON numeric
81+
constants raise ValueError. Unsupported overloads raise TypeError or
82+
NotImplementedError.
83+
84+
## JSON boundaries
85+
86+
`JavaScript.JSON.parse` creates Object, Array and String values, and binary64
87+
Python floats for numbers. Negative zero and integer rounding follow the number
88+
model. It uses the standard-library decoder's numeric hooks because standalone
89+
`JSON.parse` intentionally preserves Python integers. The companion's serializer
90+
provides string escaping; its public decoder exception type is shared.
91+
92+
`stringify` emits compact JSON by default, orders object keys, writes non-finite
93+
numbers as null, omits undefined/callable object properties, and writes them as
94+
null in arrays. Top-level undefined/callables return undefined. Shared references
95+
are allowed; cycles raise TypeError. A numeric/string `space` is capped at ten
96+
characters. Lone surrogates are escaped. Plain Python mappings/lists/tuples are
97+
accepted as a convenience.
98+
99+
Revivers, replacer functions/property lists, `toJSON` hooks, symbols and BigInt
100+
are unsupported. A non-null replacer or callable own toJSON hook raises
101+
NotImplementedError. Byte input is a standalone JSON extension and is rejected
102+
by the runtime facade. The facade does not change standalone `import JSON` or
103+
Fetch's `response.json()` result types.
104+
105+
## Fetch and Promise boundaries
106+
107+
The [standalone Fetch documentation](https://github.com/outside-labs/JavaScript-Fetch-API)
108+
is the complete contract. Core HTTP(S) requests, buffered bodies, headers,
109+
redirects, promises and await work. Browser policies, streaming, abort, cookies
110+
and browser globals are not provided. Promise callbacks use a background Python
111+
thread, not JavaScript microtask scheduling. Rejections use Python exceptions;
112+
handlers return values or Promise objects, rather than raw Python coroutines.
113+
114+
## Future translation
115+
116+
The runtime deliberately keeps recognizable call shapes, but a Python program
117+
using it is not automatically safe to translate. The future translator must
118+
resolve bindings and validate an approved Python AST subset. It must diagnose
119+
unsupported operators and Python-only constructs before emitting JavaScript.

‎pyproject.toml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ build-backend = "uv_build"
3030

3131
[tool.uv.build-backend]
3232
module-name = "JavaScript"
33+
source-include = [
34+
"tests/test_*.py",
35+
"tests/fixtures/conformance.mjs",
36+
"docs/compatibility.md",
37+
"scripts/install_distribution.py",
38+
"CHANGELOG.md",
39+
"RELEASING.md",
40+
]

0 commit comments

Comments
 (0)