Skip to content

Commit 0b2fa5d

Browse files
committed
Expanded Python API and tidyup
1 parent 691aa38 commit 0b2fa5d

File tree

3 files changed

+189
-88
lines changed

3 files changed

+189
-88
lines changed

README.pypi.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ To install:
2323
pip install nodejs-bin
2424
```
2525

26+
You can specify the Node.js version to install with:
27+
28+
```shell
29+
pip install nodejs-bin==<version>
30+
31+
# Example:
32+
pip install nodejs-bin==16.15.1
33+
```
34+
2635
To run Node.js from the command line, use:
2736

2837
```shell
@@ -33,21 +42,40 @@ node
3342

3443
`npm` and `npx` are also available as `python -m nodejs.npm` and `python -m nodejs.npx`.
3544

36-
To run Node.js from a Python program:
45+
To run Node.js from a Python program and return the exit code:
3746

3847
```python
39-
from nodejs import node, npm, npm
48+
from nodejs import node, npm, npx
4049

4150
# Run Node.js and return the exit code.
42-
node.run(['script.js', 'arg1', ...])
51+
node.run(['script.js', 'arg1', ...], **kwargs)
4352

4453
# Run npm and return the exit code.
45-
npm.run(['command', 'arg1', ...])
54+
npm.run(['command', 'arg1', ...], **kwargs)
4655

4756
# Run npx and return the exit code.
48-
npx.run(['command', 'arg1', ...])
57+
npx.run(['command', 'arg1', ...], **kwargs)
58+
```
59+
60+
The `run(args, **kwargs)` functions wrap [`subprocess.call()`](https://docs.python.org/3/library/subprocess.html#subprocess.call), passes though all `kwargs` and returns the exit code of the process.
61+
62+
Additionally, to start a Node.js process and return a `subprocess.Popen` object, you can use the `start(args, **kwargs)` functions:
63+
64+
```python
65+
from nodejs import node, npm, npx
66+
67+
# Start Node.js and return the Popen object.
68+
node_process = node.start(['script.js', 'arg1', ...], **kwargs)
69+
70+
# Start npm and return the Popen object.
71+
npm_process = npm.start(['command', 'arg1', ...], **kwargs)
72+
73+
# Start npx and return the Popen object.
74+
npx_process = npx.start(['command', 'arg1', ...], **kwargs)
4975
```
5076

77+
The `start(args, **kwargs)` functions wrap [`subprocess.Popen()`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen), passes though all `kwargs` and returns a [`Popen` object](https://docs.python.org/3/library/subprocess.html#popen-objects).
78+
5179
Alternatively use `sys.executable` to locate the Python binary to invoke. For example:
5280

5381
```python
@@ -56,7 +84,7 @@ import sys, subprocess
5684
subprocess.call([sys.executable, "-m", "nodejs"])
5785
```
5886

59-
Additionally, the standard `node`, `npm` and `npx` commands are also added to your Python environment's `bin` directory. This is usually on your `PATH` and so they should be available in your shell environment.
87+
Finally, the standard `node`, `npm` and `npx` commands are also added to your Python environment's `bin` directory. This is usually on your `PATH` and so they should be available in your shell environment.
6088

6189
License
6290
-------

make_wheels.py

Lines changed: 153 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,59 @@
11
import os
22
import hashlib
33
import urllib.request
4+
import libarchive
45
from email.message import EmailMessage
5-
from wheel.wheelfile import WheelFile, get_zipinfo_datetime
6+
from wheel.wheelfile import WheelFile
67
from zipfile import ZipInfo, ZIP_DEFLATED
7-
import libarchive # from libarchive-c
8+
from inspect import cleandoc
9+
10+
11+
# Versions to build if run as a script:
12+
BUILD_VERSIONS = ('14.19.3', '16.15.1', '18.4.0')
13+
14+
# Suffix to append to the Wheel
15+
# For pre release versions this should be 'aN', e.g. 'a1'
16+
# For release versions this should be ''
17+
# If replacing a release version, this should be a build tag '-N', e.g. '-1'.
18+
# See https://peps.python.org/pep-0427/#file-name-convention for details.
19+
BUILD_SUFFIX = 'a2'
20+
21+
# Main binary for node
22+
# Path of binary inn downloaded distribution to match
23+
NODE_BINS = ('bin/node', 'node.exe')
24+
25+
# Other binaries
26+
# key: path of binary inn downloaded distribution to match
27+
# value: tuple of (
28+
# <name>,
29+
# <True = is a link to script to run with node, False = is executable>
30+
# )
31+
NODE_OTHER_BINS = {
32+
'bin/npm': ('npm', True),
33+
'npm.cmd': ('npm', False),
34+
'bin/npx': ('npx', True),
35+
'npx.cmd': ('npx', False),
36+
'bin/corepack': ('corepack', True),
37+
'corepack.cmd': ('corepack', False),
38+
}
39+
40+
# Mapping of node platforms to Python platforms
41+
PLATFORMS = {
42+
'win-x86': 'win32',
43+
'win-x64': 'win_amd64',
44+
'darwin-x64': 'macosx_10_9_x86_64',
45+
'darwin-arm64': 'macosx_11_0_arm64',
46+
'linux-x64': 'manylinux_2_12_x86_64.manylinux2010_x86_64',
47+
'linux-armv7l': 'manylinux_2_17_armv7l.manylinux2014_armv7l',
48+
'linux-arm64': 'manylinux_2_17_aarch64.manylinux2014_aarch64',
49+
}
850

951

1052
class ReproducibleWheelFile(WheelFile):
1153
def writestr(self, zinfo, *args, **kwargs):
1254
if not isinstance(zinfo, ZipInfo):
1355
raise ValueError("ZipInfo required")
14-
zinfo.date_time = (1980,1,1,0,0,0)
56+
zinfo.date_time = (1980, 1, 1, 0, 0, 0)
1557
zinfo.create_system = 3
1658
super().writestr(zinfo, *args, **kwargs)
1759

@@ -47,10 +89,10 @@ def write_wheel(out_dir, *, name, version, tag, metadata, description, contents,
4789
dist_info = f'{name_snake}-{version}.dist-info'
4890
return write_wheel_file(os.path.join(out_dir, wheel_name), {
4991
**contents,
50-
f'{dist_info}/entry_points.txt': ("""\
51-
[console_scripts]
52-
{entry_points}
53-
""".format(entry_points='\n'.join([f'{k} = {v}' for k, v in entry_points.items()] if entry_points else []))).encode('ascii'),
92+
f'{dist_info}/entry_points.txt': (cleandoc("""
93+
[console_scripts]
94+
{entry_points}
95+
""").format(entry_points='\n'.join([f'{k} = {v}' for k, v in entry_points.items()] if entry_points else []))).encode('ascii'),
5496
f'{dist_info}/METADATA': make_message({
5597
'Metadata-Version': '2.1',
5698
'Name': name,
@@ -66,21 +108,15 @@ def write_wheel(out_dir, *, name, version, tag, metadata, description, contents,
66108
})
67109

68110

69-
NODE_BINS = ('bin/node', 'node.exe')
70-
NODE_OTHER_BINS = {
71-
'bin/npm': ('npm', True),
72-
'npm.cmd': ('npm', False),
73-
'bin/npx': ('npx', True),
74-
'npx.cmd': ('npx', False),
75-
'bin/corepack': ('corepack', True),
76-
'corepack.cmd': ('corepack', False),
77-
}
78-
79-
80-
def write_nodejs_wheel(out_dir, *, version, platform, archive):
111+
def write_nodejs_wheel(out_dir, *, node_version, version, platform, archive):
81112
contents = {}
82113
entry_points = {}
83-
contents['nodejs/__init__.py'] = f'__version__ = "{version}"\n'.encode('ascii')
114+
contents['nodejs/__init__.py'] = cleandoc(f"""
115+
from .node import path, main, run, start
116+
117+
__version__ = "{version}"
118+
node_version = "{node_version}"
119+
""").encode('ascii')
84120

85121
with libarchive.memory_reader(archive) as archive:
86122
for entry in archive:
@@ -94,53 +130,83 @@ def write_nodejs_wheel(out_dir, *, version, platform, archive):
94130

95131
if entry_name in NODE_BINS:
96132
entry_points['node'] = 'nodejs.node:main'
97-
contents['nodejs/node.py'] = f'''\
98-
import os, sys, subprocess
99-
def run(args):
100-
return subprocess.call([
101-
os.path.join(os.path.dirname(__file__), "{entry_name}"),
102-
*args
103-
])
104-
def main():
105-
sys.exit(run(sys.argv[1:]))
106-
if __name__ == '__main__':
107-
main()
108-
'''.encode('ascii')
109-
contents['nodejs/__main__.py'] = f'''\
110-
from .node import main
111-
if __name__ == '__main__':
112-
main()
113-
'''.encode('ascii')
133+
contents['nodejs/node.py'] = cleandoc(f"""
134+
import os, sys, subprocess
135+
136+
path = os.path.join(os.path.dirname(__file__), "{entry_name}")
137+
138+
def run(args, **kwargs):
139+
return subprocess.call([
140+
path,
141+
*args
142+
], **kwargs)
143+
144+
def start(args, **kwargs):
145+
return subprocess.Popen([
146+
path,
147+
*args
148+
], **kwargs)
149+
150+
def main():
151+
sys.exit(run(sys.argv[1:]))
152+
153+
if __name__ == '__main__':
154+
main()
155+
""").encode('ascii')
156+
contents['nodejs/__main__.py'] = cleandoc(f"""
157+
from .node import main
158+
159+
if __name__ == '__main__':
160+
main()
161+
""").encode('ascii')
114162
elif entry_name in NODE_OTHER_BINS and NODE_OTHER_BINS[entry_name][1]:
115163
entry_points[NODE_OTHER_BINS[entry_name][0]] = f'nodejs.{NODE_OTHER_BINS[entry_name][0]}:main'
116164
script_name = '/'.join(os.path.normpath(os.path.join(os.path.dirname(entry.name), entry.linkpath)).split('/')[1:])
117-
contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = f'''\
118-
import os, sys
119-
from .node import run as run_node
120-
def run(args):
121-
return run_node([
122-
os.path.join(os.path.dirname(__file__), "{script_name}"),
123-
*args
124-
])
125-
def main():
126-
sys.exit(run(sys.argv[1:]))
127-
if __name__ == '__main__':
128-
main()
129-
'''.encode('ascii')
165+
contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = cleandoc(f"""
166+
import os, sys
167+
from . import node
168+
169+
def run(args, **kwargs):
170+
return node.run([
171+
os.path.join(os.path.dirname(__file__), "{script_name}"),
172+
*args
173+
], **kwargs)
174+
175+
def start(args, **kwargs):
176+
return node.start([
177+
os.path.join(os.path.dirname(__file__), "{script_name}"),
178+
*args
179+
], **kwargs)
180+
181+
def main():
182+
sys.exit(run(sys.argv[1:]))
183+
184+
if __name__ == '__main__':
185+
main()
186+
""").encode('ascii')
130187
elif entry_name in NODE_OTHER_BINS:
131188
entry_points[NODE_OTHER_BINS[entry_name][0]] = f'nodejs.{NODE_OTHER_BINS[entry_name][0]}:main'
132-
contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = f'''\
133-
import os, sys, subprocess
134-
def run(args):
135-
return subprocess.call([
136-
os.path.join(os.path.dirname(__file__), "{entry_name}"),
137-
*args
138-
])
139-
def main():
140-
sys.exit(run(sys.argv[1:]))
141-
if __name__ == '__main__':
142-
main()
143-
'''.encode('ascii')
189+
contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = cleandoc(f"""
190+
import os, sys, subprocess
191+
192+
def run(args, **kwargs):
193+
return subprocess.call([
194+
os.path.join(os.path.dirname(__file__), "{entry_name}"),
195+
*args
196+
], **kwargs)
197+
198+
def start(args, **kwargs):
199+
return subprocess.Popen([
200+
os.path.join(os.path.dirname(__file__), "{entry_name}"),
201+
*args
202+
], **kwargs)
203+
204+
def main():
205+
sys.exit(run(sys.argv[1:]))
206+
207+
if __name__ == '__main__':
208+
main()
209+
""").encode('ascii')
144210

145211
with open('README.pypi.md') as f:
146212
description = f.read()
@@ -167,34 +233,40 @@ def main():
167233
)
168234

169235

170-
def main():
171-
print('Making Node.js Wheels')
172-
node_version = '16.15.1'
173-
out_version = f'{node_version}a1'
174-
175-
for node_platform, python_platform in {
176-
'win-x86': 'win32',
177-
'win-x64': 'win_amd64',
178-
'darwin-x64': 'macosx_10_9_x86_64',
179-
'darwin-arm64': 'macosx_11_0_arm64',
180-
'linux-x64': 'manylinux_2_12_x86_64.manylinux2010_x86_64',
181-
'linux-armv7l': 'manylinux_2_17_armv7l.manylinux2014_armv7l',
182-
'linux-arm64': 'manylinux_2_17_aarch64.manylinux2014_aarch64',
183-
}.items():
236+
def make_nodejs_version(node_version, suffix=''):
237+
wheel_version = f'{node_version}{suffix}'
238+
print('--')
239+
print('Making Node.js Wheels for version', node_version)
240+
if suffix:
241+
print('Suffix:', suffix)
242+
243+
for node_platform, python_platform in PLATFORMS.items():
184244
print(f'- Making Wheel for {node_platform}')
185245
node_url = f'https://nodejs.org/dist/v{node_version}/node-v{node_version}-{node_platform}.' + \
186246
('zip' if node_platform.startswith('win-') else 'tar.xz')
187-
with urllib.request.urlopen(node_url) as request:
188-
node_archive = request.read()
189-
print(f' {hashlib.sha256(node_archive).hexdigest()} {node_url}')
247+
248+
try:
249+
with urllib.request.urlopen(node_url) as request:
250+
node_archive = request.read()
251+
print(f' {node_url}')
252+
print(f' {hashlib.sha256(node_archive).hexdigest()}')
253+
except urllib.error.HTTPError as e:
254+
print(f' {e.code} {e.reason}')
255+
print(f' Skipping {node_platform}')
256+
continue
190257

191258
wheel_path = write_nodejs_wheel('dist/',
192-
version=out_version,
259+
node_version=node_version,
260+
version=wheel_version,
193261
platform=python_platform,
194262
archive=node_archive)
195263
with open(wheel_path, 'rb') as wheel:
196-
print(f' {hashlib.sha256(wheel.read()).hexdigest()} {wheel_path}')
264+
print(f' {wheel_path}')
265+
print(f' {hashlib.sha256(wheel.read()).hexdigest()}')
197266

267+
def main():
268+
for node_version in BUILD_VERSIONS:
269+
make_nodejs_version(node_version, suffix=BUILD_SUFFIX)
198270

199271
if __name__ == '__main__':
200272
main()

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
wheel
22
twine
3-
libarchive-c
3+
libarchive-c
4+
pytest

0 commit comments

Comments
 (0)