-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Expose nanobind's stubgen as a
py_binary
(#35)
* Expose nanobind's stubgen as a py_binary Adds `typing_extensions` as a unconditional dependency for the stubgen target. The plan is to wrap the py_binary into a nanobind stubgen rule that lives in the same file as all other rules. * Add docstring, inline arg construction for py_binary Everything is being forwarded to the py_binary that previously existed in the top-level BUILD file. There are still some difficulties locating the module object, since it exists in multiple places. Maybe it too needs to be sourced from $BINDIR. * Use template string for all files, assign labels to variables * Move stubgen to nanobind package, depend on it in stubgen target Stubgen does not understand file system paths to modules, so we need to make an indirection and convert paths to nanobind extension targets to Pythonic module paths via a Python script. * Add stubgen wrapper script to create stubs directly in $(BINDIR) Extracts runfiles dir and bindir from the invoker's script path, and proceeds to put the generated stub file directly into the $(BINDIR). What is left outstanding is to support pattern and marker files, but that has lower prio for now. * Deduce bindir from stubgen wrapper CWD This makes the bindir available from Windows. Also, harden the stub output path generation on Windows in the case that the binary location in runfiles is not a symlink. Also also, add a pattern file to the binary's data deps if provided, ensuring that the file exists in the repo as an addressable target, and is visible to the stubgen wrapper in the runfiles dir.
- Loading branch information
1 parent
0656359
commit dc9d28d
Showing
8 changed files
with
290 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import os | ||
import sys | ||
|
||
from pathlib import Path | ||
from typing import Union | ||
|
||
from stubgen import main | ||
from python.runfiles import runfiles | ||
|
||
DEBUG = bool(os.getenv("DEBUG", False)) | ||
RLOCATION_ROOT = Path("_main") # the Python path root under the script's runfiles. | ||
|
||
def get_runfiles_dir(path: Union[str, os.PathLike]): | ||
"""Obtain the runfiles root from the Python script path.""" | ||
ppath = Path(path) | ||
for p in ppath.parents: | ||
if p.parts[-1].endswith("runfiles"): | ||
return p | ||
raise RuntimeError("could not locate runfiles directory") | ||
|
||
|
||
def get_bindir(path: Union[str, os.PathLike]): | ||
"""Obtain $(BINDIR) as an absolute path, from the current working directory. | ||
NB: runfiles are not necessarily in the build tree on Windows, | ||
so this needs to be deduced from the CWD of the script. | ||
""" | ||
ppath = Path(path) | ||
for p in ppath.parents: | ||
if p.parts[-1].endswith("bin"): | ||
return p | ||
raise RuntimeError("could not locate $(BINDIR)") | ||
|
||
|
||
def convert_path_to_module(path: Union[str, os.PathLike]): | ||
""" | ||
Converts a shared object file name to a Python module name | ||
understood by importlib. | ||
Example: | ||
For a shared lib pkg/foo.so, this returns pkg.foo. | ||
""" | ||
pp = Path(path) | ||
# this trick strips up to two extensions from the file name. | ||
# Since possible extensions at this point are | ||
# .so, .abi3.so, and .pyd, this path always gives us the | ||
# name of the shared lib without any extension. | ||
extless = pp.with_name(pp.with_suffix("").stem) | ||
# TODO: Normalize to snakecase | ||
return ".".join(extless.parts) | ||
|
||
|
||
def wrapper(): | ||
""" | ||
A small wrapper to convert nanobind extension targets to module names | ||
relative to the runfiles directory. | ||
nanobind's stubgen script can only deal with module names | ||
found on PYTHONPATH. Since Make variable expansion in Bazel | ||
only works for paths, this does us no good. | ||
The target extension and output file should be figured out directly | ||
from the user's nanobind_stubgen rule definition - in fact, making | ||
the user fiddle with rules is error-prone and unhelpful if they | ||
have no Bazel experience. | ||
Goes through the script's argv, finds the module name(s), | ||
and converts each of them to a valid Python 3 module name. | ||
""" | ||
script, *args = sys.argv | ||
runfiles_dir = get_runfiles_dir(script) | ||
bindir = get_bindir(os.getcwd()) | ||
if DEBUG: | ||
print(f"runfiles_dir = {runfiles_dir}") | ||
print(f"bindir = {bindir}") | ||
fname = "" | ||
for i, arg in enumerate(args): | ||
if arg.startswith("-m"): | ||
fname = args.pop(i + 1) | ||
if not fname.endswith((".so", ".pyd")): | ||
raise ValueError( | ||
f"invalid extension file {fname!r}: " | ||
"only shared object files with extensions " | ||
".so, .abi3.so, or .pyd are supported" | ||
) | ||
modname = convert_path_to_module(fname) | ||
args.insert(i + 1, modname) | ||
|
||
if "-o" not in args: | ||
ext_path = runfiles_dir / fname | ||
if DEBUG: | ||
print(f"ext_path = {ext_path}") | ||
if (ext_path).is_symlink(): | ||
# Path.readlink() is available on Python 3.9+ only. | ||
objfile = Path(os.readlink(ext_path)) | ||
else: | ||
objfile = bindir / Path(fname).relative_to(RLOCATION_ROOT) | ||
if not objfile.exists(): | ||
raise RuntimeError("could not locate original path to object file") | ||
|
||
stub_outpath = objfile.with_suffix("").with_suffix(".pyi") | ||
if DEBUG: | ||
print(f"stub_outpath = {stub_outpath}") | ||
|
||
args.extend(["-o", str(stub_outpath)]) | ||
else: | ||
# we have an output file, use its path instead relative to $(BINDIR), | ||
# but in absolute form. | ||
idx = args.index("-o") | ||
args[idx + 1] = str(bindir / args[idx + 1]) | ||
|
||
if "-M" in args: | ||
# fix up the path to the marker file relative to $(BINDIR). | ||
idx = args.index("-M") | ||
args[idx + 1] = str(bindir / args[idx + 1]) | ||
|
||
main(args) | ||
|
||
|
||
if __name__ == "__main__": | ||
wrapper() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
""" | ||
A version of the Python sdist Bazel template found in | ||
https://github.com/bazelbuild/rules_python/blob/main/python/private/pypi/deps.bzl , | ||
specialized on the ``typing_extensions`` package. | ||
|
||
# Copyright 2023 The Bazel Authors. All rights reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
""" | ||
|
||
load("@rules_python//python:defs.bzl", "py_library") | ||
|
||
package(default_visibility = ["//visibility:public"]) | ||
|
||
py_library( | ||
name = "lib", | ||
srcs = ["src/typing_extensions.py"], | ||
data = [ | ||
"CHANGELOG.md", | ||
"LICENSE", | ||
"PKG-INFO", | ||
"README.md", | ||
"pyproject.toml", | ||
], | ||
# This makes the source directory a top-level in the Python import | ||
# search path for anything that depends on this. | ||
imports = ["src"], | ||
) |