Skip to content

Commit e2d0748

Browse files
committed
feat: add support for nodejs-wheel
1 parent 9edbff0 commit e2d0748

File tree

7 files changed

+92
-48
lines changed

7 files changed

+92
-48
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ Use the package manager [pip](https://pip.pypa.io/en/stable/) to install pyright
1616
pip install pyright
1717
```
1818

19+
> [!TIP]
20+
> It's highly recommended to install `pyright` with the `nodejs` extra which uses [`nodejs-wheel`](https://pypi.org/project/nodejs-wheel-binaries/) to
21+
> download Node.js binaries as it is more reliable than the default [`nodeenv`](https://pypi.org/project/nodeenv/) solution.
22+
>
23+
> ```bash
24+
> pip install pyright[nodejs]
25+
> ```
26+
27+
1928
## Usage
2029
2130
Pyright can be invoked using two different methods

pyright/node.py

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,26 @@
88
import logging
99
import platform
1010
import subprocess
11-
from typing import Any, Dict, Tuple, Union, Mapping, Optional, cast
11+
import importlib.util
12+
from typing import Any, Dict, Tuple, Union, Mapping, Optional, NamedTuple, cast
1213
from pathlib import Path
1314
from functools import lru_cache
15+
from typing_extensions import Literal, assert_never
1416

1517
from . import errors
16-
from .types import Binary, Target, Strategy, check_target
18+
from .types import Target, check_target
1719
from .utils import env_to_bool, get_bin_dir, get_env_dir, maybe_decode
1820

1921
log: logging.Logger = logging.getLogger(__name__)
2022

2123
ENV_DIR: Path = get_env_dir()
2224
BINARIES_DIR: Path = get_bin_dir(env_dir=ENV_DIR)
2325
USE_GLOBAL_NODE = env_to_bool('PYRIGHT_PYTHON_GLOBAL_NODE', default=True)
26+
USE_NODEJS_WHEEL = env_to_bool('PYRIGHT_PYTHON_NODEJS_WHEEL', default=True)
2427
NODE_VERSION = os.environ.get('PYRIGHT_PYTHON_NODE_VERSION', default=None)
2528
VERSION_RE = re.compile(r'\d+\.\d+\.\d+')
2629

2730

28-
def _ensure_available(target: Target) -> Binary:
29-
"""Ensure the target node executable is available"""
30-
path = None
31-
if USE_GLOBAL_NODE:
32-
path = _get_global_binary(target)
33-
34-
if path is not None:
35-
return Binary(path=path, strategy=Strategy.GLOBAL)
36-
37-
return Binary(path=_ensure_node_env(target), strategy=Strategy.NODEENV)
38-
39-
4031
def _is_windows() -> bool:
4132
return platform.system().lower() == 'windows'
4233

@@ -97,31 +88,83 @@ def _install_node_env() -> None:
9788
subprocess.run(args, check=True)
9889

9990

91+
class GlobalStrategy(NamedTuple):
92+
type: Literal['global']
93+
path: Path
94+
95+
96+
class NodeJSWheelStrategy(NamedTuple):
97+
type: Literal['nodejs_wheel']
98+
99+
100+
class NodeenvStrategy(NamedTuple):
101+
type: Literal['nodeenv']
102+
path: Path
103+
104+
105+
Strategy = Union[GlobalStrategy, NodeJSWheelStrategy, NodeenvStrategy]
106+
107+
108+
def _resolve_strategy(target: Target) -> Strategy:
109+
if USE_NODEJS_WHEEL:
110+
if importlib.util.find_spec('nodejs_wheel') is not None:
111+
log.debug('Using nodejs_wheel package for resolving binaries')
112+
return NodeJSWheelStrategy(type='nodejs_wheel')
113+
114+
if USE_GLOBAL_NODE:
115+
path = _get_global_binary(target)
116+
if path is not None:
117+
log.debug('Using global %s binary', target)
118+
return GlobalStrategy(type='global', path=path)
119+
120+
log.debug('Installing binaries using nodeenv')
121+
return NodeenvStrategy(type='nodeenv', path=_ensure_node_env(target))
122+
123+
100124
def run(
101125
target: Target, *args: str, **kwargs: Any
102126
) -> Union['subprocess.CompletedProcess[bytes]', 'subprocess.CompletedProcess[str]']:
103127
check_target(target)
104-
binary = _ensure_available(target)
105-
env = kwargs.pop('env', None) or os.environ.copy()
106128

107-
if binary.strategy == Strategy.NODEENV:
129+
strategy = _resolve_strategy(target)
130+
if strategy.type == 'global':
131+
node_args = [str(strategy.path), *args]
132+
log.debug('Running global node command with args: %s', node_args)
133+
return cast(
134+
'subprocess.CompletedProcess[str] | subprocess.CompletedProcess[bytes]',
135+
subprocess.run(node_args, **kwargs),
136+
)
137+
elif strategy.type == 'nodejs_wheel':
138+
import nodejs_wheel
139+
140+
if target == 'node':
141+
return cast(
142+
'subprocess.CompletedProcess[str] | subprocess.CompletedProcess[bytes]',
143+
nodejs_wheel.node(args, return_completed_process=True, **kwargs),
144+
)
145+
elif target == 'npm':
146+
return cast(
147+
'subprocess.CompletedProcess[str] | subprocess.CompletedProcess[bytes]',
148+
nodejs_wheel.npm(args, return_completed_process=True, **kwargs),
149+
)
150+
else:
151+
assert_never(target)
152+
elif strategy.type == 'nodeenv':
153+
env = kwargs.pop('env', None) or os.environ.copy()
108154
env.update(get_env_variables())
109155

110156
# If we're using `nodeenv` to resolve the node binary then we also need
111157
# to ensure that `node` is in the PATH so that any install scripts that
112158
# assume it is present will work.
113-
env.update(PATH=_update_path_env(env=env, target_bin=binary.path.parent))
114-
node_args = [str(binary.path), *args]
115-
elif binary.strategy == Strategy.GLOBAL:
116-
node_args = [str(binary.path), *args]
159+
env.update(PATH=_update_path_env(env=env, target_bin=strategy.path.parent))
160+
node_args = [str(strategy.path), *args]
161+
log.debug('Running nodeenv command with args: %s', node_args)
162+
return cast(
163+
'subprocess.CompletedProcess[str] | subprocess.CompletedProcess[bytes]',
164+
subprocess.run(node_args, **kwargs),
165+
)
117166
else:
118-
raise RuntimeError(f'Unknown strategy: {binary.strategy}')
119-
120-
log.debug('Running node command with args: %s', node_args)
121-
return cast(
122-
'subprocess.CompletedProcess[str] | subprocess.CompletedProcess[bytes]',
123-
subprocess.run(node_args, env=env, **kwargs),
124-
)
167+
assert_never(strategy)
125168

126169

127170
def version(target: Target) -> Tuple[int, ...]:

pyright/types.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
1+
from __future__ import annotations
2+
13
import sys
2-
from enum import Enum
3-
from typing import Any, NamedTuple
4-
from pathlib import Path
4+
from typing import Any
55

66
if sys.version_info >= (3, 8):
77
from typing import Literal
88
else:
99
from typing_extensions import Literal
1010

1111

12-
class Strategy(int, Enum):
13-
GLOBAL = 0
14-
NODEENV = 1
15-
16-
17-
class Binary(NamedTuple):
18-
path: Path
19-
strategy: Strategy
20-
21-
2212
# we have to define twice to support runtime type checking
2313
# on python < 3.7 as typing.get_args is not available
2414
Target = Literal['node', 'npm']

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
nodeenv>=1.6.0
2-
typing-extensions>=3.7; python_version < '3.8'
2+
typing-extensions>=4.1

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
extras = {
2727
'dev': [
2828
'twine>=3.4.1',
29-
]
29+
],
30+
'nodejs': ['nodejs-wheel-binaries'],
3031
}
3132

3233
setup(

tests/conftest.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from pathlib import Path
44

55
import pytest
6-
7-
from pyright import node
6+
import nodejs_wheel
87

98

109
@pytest.fixture(name='tmp_path')
@@ -20,9 +19,10 @@ def tmp_path_fixture(tmp_path: Path) -> Iterator[Path]:
2019

2120
@pytest.fixture(name='node', scope='session')
2221
def node_fixture() -> str:
23-
return str(
24-
node._ensure_available('node').path # pyright: ignore[reportPrivateUsage]
25-
)
22+
if os.name == 'nt':
23+
return str(Path(nodejs_wheel.__file__).parent / 'node.exe')
24+
25+
return str(Path(nodejs_wheel.__file__).parent / 'bin' / 'node')
2626

2727

2828
@pytest.fixture(autouse=True)

tests/test_node.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def test_run_env_argument(tmp_path: Path) -> None:
7777

7878
@mock.patch('pyright.node.NODE_VERSION', '13.1.0')
7979
@mock.patch('pyright.node.USE_GLOBAL_NODE', False)
80+
@mock.patch('pyright.node.USE_NODEJS_WHEEL', False)
8081
def test_node_version_env() -> None:
8182
"""Ensure the custom version is respected."""
8283
proc = pyright.node.run(

0 commit comments

Comments
 (0)