Skip to content

Commit c36eeb0

Browse files
authored
Merge pull request #186 from davidhewitt/add-mypy
ci: add mypy typecheck
2 parents 560b16c + ea3a4e8 commit c36eeb0

File tree

10 files changed

+204
-99
lines changed

10 files changed

+204
-99
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ jobs:
2020

2121
- run: black --check .
2222

23+
mypy:
24+
runs-on: "ubuntu-latest"
25+
steps:
26+
- uses: actions/checkout@v2
27+
28+
- name: Set up Python ${{ matrix.python-version }}
29+
uses: actions/setup-python@v2
30+
with:
31+
python-version: "3.x"
32+
33+
- run: pip install tox
34+
35+
- run: tox -e mypy
36+
2337
build:
2438
name: ${{ matrix.python-version }} ${{ matrix.platform.os }}-${{ matrix.platform.python-architecture }}
2539
runs-on: ${{ matrix.platform.os }}

mypy.ini

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# We have to manually ignore types for some packages. It would be great if
2+
# interested users want to add these types to typeshed!
3+
4+
[mypy]
5+
show_error_codes = True
6+
7+
disallow_untyped_defs = True
8+
disallow_any_unimported = True
9+
no_implicit_optional = True
10+
check_untyped_defs = True
11+
12+
warn_return_any = True
13+
warn_unused_ignores = True
14+
15+
[mypy-semantic_version.*]
16+
ignore_missing_imports = True
17+
18+
[mypy-wheel.*]
19+
ignore_missing_imports = True

setuptools_rust/build.py

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import subprocess
77
import sys
88
import sysconfig
9+
from distutils.command.build import build as CommandBuild
910
from distutils.errors import (
1011
CompileError,
1112
DistutilsExecError,
@@ -14,8 +15,9 @@
1415
)
1516
from distutils.sysconfig import get_config_var
1617
from subprocess import check_output
17-
from typing import List, NamedTuple, Optional, Tuple
18+
from typing import List, NamedTuple, Optional, cast
1819

20+
from setuptools.command.build_ext import build_ext as CommandBuildExt
1921
from setuptools.command.build_ext import get_abi3_suffix
2022

2123
from .command import RustCommand
@@ -52,7 +54,9 @@ class build_rust(RustCommand):
5254
]
5355
boolean_options = ["inplace", "debug", "release", "qbuild"]
5456

55-
def initialize_options(self):
57+
plat_name: Optional[str]
58+
59+
def initialize_options(self) -> None:
5660
super().initialize_options()
5761
self.inplace = None
5862
self.debug = None
@@ -62,11 +66,14 @@ def initialize_options(self):
6266
self.plat_name = None
6367
self.target = os.getenv("CARGO_BUILD_TARGET")
6468

65-
def finalize_options(self):
69+
def finalize_options(self) -> None:
6670
super().finalize_options()
6771

6872
if self.plat_name is None:
69-
self.plat_name = self.get_finalized_command("build").plat_name
73+
self.plat_name = cast(
74+
CommandBuild, self.get_finalized_command("build")
75+
).plat_name
76+
assert isinstance(self.plat_name, str)
7077

7178
# Inherit settings from the `build_ext` command
7279
self.set_undefined_options(
@@ -77,6 +84,8 @@ def finalize_options(self):
7784
)
7885

7986
def get_target_info(self) -> "_TargetInfo":
87+
assert self.plat_name is not None
88+
8089
# If we are on a 64-bit machine, but running a 32-bit Python, then
8190
# we'll target a 32-bit Rust build.
8291
# Automatic target detection can be overridden via the CARGO_BUILD_TARGET
@@ -115,7 +124,8 @@ def get_target_info(self) -> "_TargetInfo":
115124
% cross_compile_info.host_type
116125
)
117126

118-
return _TargetInfo.for_triple(self.target)
127+
# FIXME!
128+
return _TargetInfo.for_triple(self.target) # type: ignore[arg-type]
119129

120130
def get_nix_cross_compile_info(self) -> Optional["_CrossCompileInfo"]:
121131
# See https://github.com/PyO3/setuptools-rust/issues/138
@@ -143,7 +153,9 @@ def get_nix_cross_compile_info(self) -> Optional["_CrossCompileInfo"]:
143153

144154
return _CrossCompileInfo(host_type, cross_lib, linker, linker_args)
145155

146-
def run_for_extension(self, ext: RustExtension):
156+
def run_for_extension(self, ext: RustExtension) -> None:
157+
assert self.plat_name is not None
158+
147159
arch_flags = os.getenv("ARCHFLAGS")
148160
universal2 = False
149161
if self.plat_name.startswith("macosx-") and arch_flags:
@@ -156,15 +168,15 @@ def run_for_extension(self, ext: RustExtension):
156168
arm64_dylib_paths, x86_64_dylib_paths
157169
):
158170
fat_dylib_path = arm64_dylib.replace("aarch64-apple-darwin/", "")
159-
self.create_universal2_binary(
160-
fat_dylib_path, [arm64_dylib, x86_64_dylib]
161-
)
162-
dylib_paths.append((target_fname, fat_dylib_path))
171+
create_universal2_binary(fat_dylib_path, [arm64_dylib, x86_64_dylib])
172+
dylib_paths.append(_BuiltModule(target_fname, fat_dylib_path))
163173
else:
164174
dylib_paths = self.build_extension(ext)
165175
self.install_extension(ext, dylib_paths)
166176

167-
def build_extension(self, ext: RustExtension, target_triple=None):
177+
def build_extension(
178+
self, ext: RustExtension, target_triple: Optional[str] = None
179+
) -> List["_BuiltModule"]:
168180
executable = ext.binding == Binding.Exec
169181

170182
if target_triple is None:
@@ -333,7 +345,7 @@ def build_extension(self, ext: RustExtension, target_triple=None):
333345

334346
path = os.path.join(artifactsdir, name)
335347
if os.access(path, os.X_OK):
336-
dylib_paths.append((dest, path))
348+
dylib_paths.append(_BuiltModule(dest, path))
337349
else:
338350
raise DistutilsExecError(
339351
"Rust build failed; "
@@ -351,7 +363,7 @@ def build_extension(self, ext: RustExtension, target_triple=None):
351363

352364
try:
353365
dylib_paths.append(
354-
(
366+
_BuiltModule(
355367
ext.name,
356368
next(glob.iglob(os.path.join(artifactsdir, wildcard_so))),
357369
)
@@ -362,15 +374,18 @@ def build_extension(self, ext: RustExtension, target_triple=None):
362374
)
363375
return dylib_paths
364376

365-
def install_extension(self, ext: RustExtension, dylib_paths: List[Tuple[str, str]]):
377+
def install_extension(
378+
self, ext: RustExtension, dylib_paths: List["_BuiltModule"]
379+
) -> None:
366380
executable = ext.binding == Binding.Exec
367381
debug_build = ext.debug if ext.debug is not None else self.inplace
368382
debug_build = self.debug if self.debug is not None else debug_build
369383
if self.release:
370384
debug_build = False
385+
371386
# Ask build_ext where the shared library would go if it had built it,
372387
# then copy it there.
373-
build_ext = self.get_finalized_command("build_ext")
388+
build_ext = cast(CommandBuildExt, self.get_finalized_command("build_ext"))
374389
build_ext.inplace = self.inplace
375390

376391
for module_name, dylib_path in dylib_paths:
@@ -419,55 +434,70 @@ def install_extension(self, ext: RustExtension, dylib_paths: List[Tuple[str, str
419434
os.chmod(ext_path, mode)
420435

421436
def get_dylib_ext_path(self, ext: RustExtension, target_fname: str) -> str:
422-
build_ext = self.get_finalized_command("build_ext")
437+
build_ext = cast(CommandBuildExt, self.get_finalized_command("build_ext"))
423438

424-
filename = build_ext.get_ext_fullpath(target_fname)
439+
filename: str = build_ext.get_ext_fullpath(target_fname)
425440

426441
if (ext.py_limited_api == "auto" and self._py_limited_api()) or (
427442
ext.py_limited_api
428443
):
429444
abi3_suffix = get_abi3_suffix()
430445
if abi3_suffix is not None:
431446
so_ext = get_config_var("EXT_SUFFIX")
447+
assert isinstance(so_ext, str)
432448
filename = filename[: -len(so_ext)] + get_abi3_suffix()
433449

434450
return filename
435451

436-
@staticmethod
437-
def create_universal2_binary(output_path, input_paths):
438-
# Try lipo first
439-
command = ["lipo", "-create", "-output", output_path, *input_paths]
440-
try:
441-
subprocess.check_output(command)
442-
except subprocess.CalledProcessError as e:
443-
output = e.output
444-
if isinstance(output, bytes):
445-
output = e.output.decode("latin-1").strip()
446-
raise CompileError("lipo failed with code: %d\n%s" % (e.returncode, output))
447-
except OSError:
448-
# lipo not found, try using the fat-macho library
449-
try:
450-
from fat_macho import FatWriter
451-
except ImportError:
452-
raise DistutilsExecError(
453-
"failed to locate `lipo` or import `fat_macho.FatWriter`. "
454-
"Try installing with `pip install fat-macho` "
455-
)
456-
fat = FatWriter()
457-
for input_path in input_paths:
458-
with open(input_path, "rb") as f:
459-
fat.add(f.read())
460-
fat.write_to(output_path)
461-
462452
def _py_limited_api(self) -> PyLimitedApi:
463-
bdist_wheel = self.distribution.get_command_obj("bdist_wheel", create=0)
453+
bdist_wheel = self.distribution.get_command_obj("bdist_wheel", create=False)
464454

465455
if bdist_wheel is None:
466456
# wheel package is not installed, not building a limited-api wheel
467457
return False
468458
else:
469-
bdist_wheel.ensure_finalized()
470-
return bdist_wheel.py_limited_api
459+
from wheel.bdist_wheel import bdist_wheel as CommandBdistWheel
460+
461+
bdist_wheel_command = cast(CommandBdistWheel, bdist_wheel) # type: ignore[no-any-unimported]
462+
bdist_wheel_command.ensure_finalized()
463+
return cast(PyLimitedApi, bdist_wheel_command.py_limited_api)
464+
465+
466+
def create_universal2_binary(output_path: str, input_paths: List[str]) -> None:
467+
# Try lipo first
468+
command = ["lipo", "-create", "-output", output_path, *input_paths]
469+
try:
470+
subprocess.check_output(command)
471+
except subprocess.CalledProcessError as e:
472+
output = e.output
473+
if isinstance(output, bytes):
474+
output = e.output.decode("latin-1").strip()
475+
raise CompileError("lipo failed with code: %d\n%s" % (e.returncode, output))
476+
except OSError:
477+
# lipo not found, try using the fat-macho library
478+
try:
479+
from fat_macho import FatWriter
480+
except ImportError:
481+
raise DistutilsExecError(
482+
"failed to locate `lipo` or import `fat_macho.FatWriter`. "
483+
"Try installing with `pip install fat-macho` "
484+
)
485+
fat = FatWriter()
486+
for input_path in input_paths:
487+
with open(input_path, "rb") as f:
488+
fat.add(f.read())
489+
fat.write_to(output_path)
490+
491+
492+
class _BuiltModule(NamedTuple):
493+
"""
494+
Attributes:
495+
- module_name: dotted python import path of the module
496+
- path: the location the module has been installed at
497+
"""
498+
499+
module_name: str
500+
path: str
471501

472502

473503
class _TargetInfo(NamedTuple):

setuptools_rust/clean.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import sys
21
import subprocess
2+
import sys
33

44
from .command import RustCommand
55
from .extension import RustExtension
@@ -10,11 +10,11 @@ class clean_rust(RustCommand):
1010

1111
description = "clean Rust extensions (compile/link to build directory)"
1212

13-
def initialize_options(self):
13+
def initialize_options(self) -> None:
1414
super().initialize_options()
1515
self.inplace = False
1616

17-
def run_for_extension(self, ext: RustExtension):
17+
def run_for_extension(self, ext: RustExtension) -> None:
1818
# build cargo command
1919
args = ["cargo", "clean", "--manifest-path", ext.path]
2020

setuptools_rust/command.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from abc import ABC, abstractmethod
22
from distutils.cmd import Command
33
from distutils.errors import DistutilsPlatformError
4+
from typing import List
5+
6+
from setuptools.dist import Distribution
47

58
from .extension import RustExtension
69
from .utils import get_rust_version
@@ -9,25 +12,30 @@
912
class RustCommand(Command, ABC):
1013
"""Abstract base class for commands which interact with Rust Extensions."""
1114

12-
def initialize_options(self):
13-
self.extensions = ()
15+
# Types for distutils variables which exist on all commands but seem to be
16+
# missing from https://github.com/python/typeshed/blob/master/stdlib/distutils/cmd.pyi
17+
distribution: Distribution
18+
verbose: int
19+
20+
def initialize_options(self) -> None:
21+
self.extensions: List[RustExtension] = []
1422

15-
def finalize_options(self):
23+
def finalize_options(self) -> None:
1624
self.extensions = [
1725
ext
18-
for ext in self.distribution.rust_extensions
26+
for ext in self.distribution.rust_extensions # type: ignore[attr-defined]
1927
if isinstance(ext, RustExtension)
2028
]
2129

22-
def run(self):
30+
def run(self) -> None:
2331
if not self.extensions:
2432
return
2533

2634
all_optional = all(ext.optional for ext in self.extensions)
2735
try:
2836
version = get_rust_version()
2937
if version is None:
30-
min_version = max(
38+
min_version = max( # type: ignore[type-var]
3139
filter(
3240
lambda version: version is not None,
3341
(ext.get_rust_version() for ext in self.extensions),

0 commit comments

Comments
 (0)