Skip to content

Add mypy typings to project compatible with python 3.10 + run unit tests in CI #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 13 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,19 @@ jobs:
run: |
mkdir dist
python make_wheels.py
- name: Show built files
- name: Show built wheels
run: |
ls -l dist/*
- name: Type Checking
run: |
python -m mypy
- name: Tests
run: |
for wheel in $(find dist -name "*manylinux*_x86_64.whl"); do
echo "Testing installation with wheel: ${wheel}"
pip install ${wheel}
python -m pytest
done
- uses: actions/upload-artifact@v3
with:
name: nodejs-pip-wheels
Expand Down Expand Up @@ -79,5 +89,5 @@ jobs:
run:
python -m nodejs --version
python -m nodejs.npm --version


python -m nodejs.npx --version
python -m nodejs.corepack --version
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ nodejs-cmd/*.egg-info
.DS_Store
env*/
__pycache__/
*.py[cod]
*.py[cod]
venv
node_modules
package-lock.json
package.json
45 changes: 30 additions & 15 deletions make_wheels.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import os
import hashlib
import platform
import urllib.request
import urllib.error
from typing import Dict, List, Any
import libarchive
from email.message import EmailMessage
from wheel.wheelfile import WheelFile
from zipfile import ZipInfo, ZIP_DEFLATED
from inspect import cleandoc

# To support our internal typings, we require Python3.10+, but to KISS
# for now we only support building this project with Python3.10 (this is
# a developer only requirement)
(python_major, python_minor, _python_patch) = platform.python_version_tuple()
if not (python_major == '3' and python_minor == '10'):
breakpoint()
raise Exception(f"This build script is expected to run on Python 3.10, but you are using {platform.python_version()}")


# Versions to build if run as a script:
BUILD_VERSIONS = ('14.19.3', '16.15.1', '18.4.0')
Expand Down Expand Up @@ -48,16 +59,16 @@
}


class ReproducibleWheelFile(WheelFile):
def writestr(self, zinfo, *args, **kwargs):
class ReproducibleWheelFile(WheelFile): # type: ignore[no-any-unimported]
def writestr(self, zinfo: ZipInfo, *args: Any, **kwargs: Any) -> None:
if not isinstance(zinfo, ZipInfo):
raise ValueError("ZipInfo required")
zinfo.date_time = (1980, 1, 1, 0, 0, 0)
zinfo.create_system = 3
super().writestr(zinfo, *args, **kwargs)


def make_message(headers, payload=None):
def make_message(headers: Dict[str, str | List[str]], payload: str | None =None) -> EmailMessage:
msg = EmailMessage()
for name, value in headers.items():
if isinstance(value, list):
Expand All @@ -69,8 +80,9 @@ def make_message(headers, payload=None):
msg.set_payload(payload)
return msg

WheelContents = Dict[ZipInfo | str, bytes | EmailMessage]

def write_wheel_file(filename, contents):
def write_wheel_file(filename: str, contents: WheelContents) -> str:
with ReproducibleWheelFile(filename, 'w') as wheel:
for member_info, member_source in contents.items():
if not isinstance(member_info, ZipInfo):
Expand All @@ -82,15 +94,18 @@ def write_wheel_file(filename, contents):
return filename


def write_wheel(out_dir, *, name, version, tag, metadata, description, contents, entry_points):
def write_wheel(out_dir: str, *, name: str, version: str, tag: str, metadata: Dict[str, str | List[str]], description: str, contents: WheelContents, entry_points: Dict[str, str]) -> str:
name_snake = name.replace('-', '_')
wheel_name = f'{name_snake}-{version}-{tag}.whl'
dist_info = f'{name_snake}-{version}.dist-info'
if entry_points:
contents[f'{dist_info}/entry_points.txt'] = (cleandoc("""
entry_points_entries = '\n'.join([f'{k} = {v}' for k, v in entry_points.items()] if entry_points else [])
entry_points_file_contents = cleandoc("""
[console_scripts]
{entry_points}
""").format(entry_points='\n'.join([f'{k} = {v}' for k, v in entry_points.items()] if entry_points else []))).encode('ascii'),
""").format(entry_points=entry_points_entries)
contents[f'{dist_info}/entry_points.txt'] = entry_points_file_contents.encode('ascii')

return write_wheel_file(os.path.join(out_dir, wheel_name), {
**contents,
f'{dist_info}/METADATA': make_message({
Expand All @@ -108,12 +123,12 @@ def write_wheel(out_dir, *, name, version, tag, metadata, description, contents,
})


def write_nodejs_wheel(out_dir, *, node_version, version, platform, archive):
contents = {}
entry_points = {}
init_imports = []
def write_nodejs_wheel(out_dir: str, *, node_version: str, version: str, platform: str, archive_contents: bytes) -> str:
contents: WheelContents = {}
entry_points: Dict[str, str] = {}
init_imports: List[str] = []

with libarchive.memory_reader(archive) as archive:
with libarchive.memory_reader(archive_contents) as archive:
for entry in archive:
entry_name = '/'.join(entry.name.split('/')[1:])
if entry.isdir or not entry_name:
Expand Down Expand Up @@ -286,7 +301,7 @@ def main() -> None:
)


def make_nodejs_version(node_version, suffix=''):
def make_nodejs_version(node_version: str, suffix: str='') -> None:
wheel_version = f'{node_version}{suffix}'
print('--')
print('Making Node.js Wheels for version', node_version)
Expand All @@ -312,12 +327,12 @@ def make_nodejs_version(node_version, suffix=''):
node_version=node_version,
version=wheel_version,
platform=python_platform,
archive=node_archive)
archive_contents=node_archive)
with open(wheel_path, 'rb') as wheel:
print(f' {wheel_path}')
print(f' {hashlib.sha256(wheel.read()).hexdigest()}')

def main():
def main() -> None:
for node_version in BUILD_VERSIONS:
make_nodejs_version(node_version, suffix=BUILD_SUFFIX)

Expand Down
27 changes: 27 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[mypy]

files = **/*.py

exclude = (?x)(
venv
)

check_untyped_defs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
disallow_untyped_decorators = True
disallow_any_unimported = True
warn_return_any = True
warn_unused_ignores = True
show_error_codes = True
enable_error_code = ignore-without-code
follow_imports = normal

[mypy-libarchive]
ignore_missing_imports = True

[mypy-wheel.*]
ignore_missing_imports = True

[mypy-nodejs]
ignore_missing_imports = True
6 changes: 3 additions & 3 deletions nodejs-cmd/nodejs_cmd.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from nodejs import node, npm, npx

def node_main():
def node_main() -> None:
node.main()

def npm_main():
def npm_main() -> None:
npm.main()

def npx_main():
def npx_main() -> None:
npx.main()
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
wheel
twine
libarchive-c
pytest
pytest
typing-extensions
mypy
types-setuptools
22 changes: 12 additions & 10 deletions tests/test_comand_line.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,63 @@
"Test nodejs command line"

import os, sys, subprocess
import pathlib
from pytest import CaptureFixture


THIS_DIR = os.path.dirname(os.path.abspath(__file__))


def test_runs():
def test_runs() -> None:
assert subprocess.call([sys.executable, "-m", "nodejs", "--version"]) == 0


def test_version(capfd):
def test_version(capfd: CaptureFixture) -> None:
subprocess.call([sys.executable, "-m", "nodejs", "--version"])
out, err = capfd.readouterr()
assert out.startswith('v')


def test_eval(capfd):
def test_eval(capfd: CaptureFixture) -> None:
subprocess.call([sys.executable, "-m", "nodejs", "--eval", "console.log('hello')"])
out, err = capfd.readouterr()
assert out.strip() == 'hello'


def test_eval_error(capfd):
def test_eval_error(capfd: CaptureFixture) -> None:
subprocess.call([sys.executable, "-m", "nodejs", "--eval", "console.error('error')"])
out, err = capfd.readouterr()
assert err.strip() == 'error'


def test_eval_error_exit():
def test_eval_error_exit() -> None:
ret = subprocess.call([sys.executable, "-m", "nodejs", "--eval", "process.exit(1)"])
assert ret == 1


def test_script(capfd):
def test_script(capfd: CaptureFixture) -> None:
subprocess.call([sys.executable, "-m", "nodejs", os.path.join(THIS_DIR, "test_node", "test_script.js")])
out, err = capfd.readouterr()
assert out.strip() == 'hello'


def test_args(capfd):
def test_args(capfd: CaptureFixture) -> None:
subprocess.call([sys.executable, "-m", "nodejs", os.path.join(THIS_DIR, "test_node", "test_args.js"), "hello"])
out, err = capfd.readouterr()
assert out.strip() == 'hello'


def test_npm_runs():
def test_npm_runs() -> None:
assert subprocess.call([sys.executable, "-m", "nodejs.npm", "--version"]) == 0


def test_npm_version(capfd):
def test_npm_version(capfd: CaptureFixture) -> None:
subprocess.call([sys.executable, "-m", "nodejs.npm", "--version"])
out, err = capfd.readouterr()
assert isinstance(out, str)


def test_install_package(tmp_path, capfd):
def test_install_package(tmp_path: pathlib.Path, capfd: CaptureFixture) -> None:
os.chdir(tmp_path)
subprocess.call([sys.executable, "-m", "nodejs.npm", "init", "-y"])
assert (tmp_path / 'package.json').exists()
Expand Down
17 changes: 9 additions & 8 deletions tests/test_node.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,58 @@
"Test nodejs.node"

import os
from pytest import CaptureFixture


THIS_DIR = os.path.dirname(os.path.abspath(__file__))


def test_package_installed():
def test_package_installed() -> None:
import nodejs
assert nodejs.__version__ is not None


def test_runs():
def test_runs() -> None:
from nodejs import node
assert node.call(['--version']) is 0


def test_version(capfd):
def test_version(capfd: CaptureFixture) -> None:
from nodejs import node, node_version
node.call(['--version'])
out, err = capfd.readouterr()
assert out.startswith('v')
assert out.strip() == f'v{node_version}'


def test_eval(capfd):
def test_eval(capfd: CaptureFixture) -> None:
from nodejs import node
node.call(['--eval', 'console.log("hello")'])
out, err = capfd.readouterr()
assert out.strip() == 'hello'


def test_eval_error(capfd):
def test_eval_error(capfd: CaptureFixture) -> None:
from nodejs import node
node.call(['--eval', 'console.error("error")'])
out, err = capfd.readouterr()
assert err.strip() == 'error'


def test_eval_error_exit():
def test_eval_error_exit() -> None:
from nodejs import node
ret = node.call(['--eval', 'process.exit(1)'])
assert ret == 1


def test_script(capfd):
def test_script(capfd: CaptureFixture) -> None:
from nodejs import node
node.call([os.path.join(THIS_DIR, 'test_node', 'test_script.js')])
out, err = capfd.readouterr()
assert out.strip() == 'hello'


def test_args(capfd):
def test_args(capfd: CaptureFixture) -> None:
from nodejs import node
node.call([os.path.join(THIS_DIR, 'test_node', 'test_args.js'), 'hello'])
out, err = capfd.readouterr()
Expand Down
10 changes: 6 additions & 4 deletions tests/test_npm.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
"Test nodejs.npm"

import os
import pathlib
from pytest import CaptureFixture


def test_runs():
def test_runs() -> None:
from nodejs import npm
assert npm.call(['--version']) is 0


def test_version(capfd):
def test_version(capfd: CaptureFixture) -> None:
from nodejs import npm
npm.call(['--version'])
out, err = capfd.readouterr()
assert isinstance(out, str)


def test_install_package(tmp_path, capfd):
def test_install_package(tmp_path: pathlib.Path, capfd: CaptureFixture) -> None:
from nodejs import npm, node
import json

os.chdir(tmp_path)
npm.call(['init', '-y'])
assert (tmp_path / 'package.json').exists()
Expand Down