Skip to content

Commit cf2c751

Browse files
authored
Merge pull request #230 from robotpy/update-gen
Update various generation related tooling
2 parents 00080e1 + 8276c95 commit cf2c751

File tree

12 files changed

+317
-120
lines changed

12 files changed

+317
-120
lines changed

docs/tools.rst

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,20 @@ your python package.
1515

1616
This will scan all of your defined includes directories (including those of
1717
downloaded artifacts) and output something you can paste into the ``generate``
18-
key of ``pyproject.toml``.
18+
key of ``pyproject.toml``. By default it will only show files that are not
19+
present in ``pyproject.toml`` -- to show all files use the ``--all`` argument.
20+
21+
Often there are files that you don't want to wrap. You can add them to the
22+
``pyproject.toml`` file and they will be ignored. The list accepts glob patterns
23+
supported by the fnmatch module.
24+
25+
.. code-block:: toml
26+
27+
[tool.robotpy-build]
28+
scan_headers_ignore = [
29+
"ignored_header.h",
30+
"ignore_dir/*",
31+
]
1932
2033
.. _create_gen:
2134

@@ -40,3 +53,15 @@ python package.
4053
.. code-block:: sh
4154
4255
$ python -m robotpy_build create-imports rpydemo rpydemo._rpydemo
56+
57+
Use the ``--write`` argument to write the file.
58+
59+
To write a list of ``__init__.py`` files, you can specify them in the ``pyproject.toml``
60+
file like so:
61+
62+
.. code-block:: toml
63+
64+
[tool.robotpy-build]
65+
update_init = ["rpydemo rpydemo._rpydemo"]
66+
67+
To actually update the files, run ``python setup.py update_init``.

robotpy_build/autowrap/generator_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def get_function_data(
133133
data = self._default_fn_data
134134
report_data.deferred_signatures.append((fn, is_private))
135135
elif not data.overloads:
136-
report_data.deferred_signatures.append((fn, True))
136+
report_data.deferred_signatures.append((fn, is_private))
137137
else:
138138
# When there is overload data present, we have to actually compute
139139
# the signature of every function

robotpy_build/command/_built_env.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import importlib.util
2+
import os
3+
from os.path import abspath, exists, dirname, join
4+
5+
from setuptools import Command
6+
7+
from .util import get_install_root
8+
9+
10+
class _BuiltEnv(Command):
11+
12+
user_options = [("build-lib=", "d", 'directory to "build" (copy) to')]
13+
14+
def initialize_options(self):
15+
self.build_lib = None
16+
17+
def finalize_options(self):
18+
self.set_undefined_options("build", ("build_lib", "build_lib"))
19+
20+
def setup_built_env(self):
21+
22+
# Gather information for n
23+
data = {"mapping": {}}
24+
25+
# OSX-specific: need to set DYLD_LIBRARY_PATH otherwise modules don't
26+
# work. Luckily, that information was computed when building the
27+
# extensions...
28+
env = os.environ.copy()
29+
dyld_path = set()
30+
31+
# Requires information from build_ext to work
32+
build_ext = self.get_finalized_command("build_ext")
33+
if build_ext.inplace:
34+
data["out"] = get_install_root(self)
35+
else:
36+
data["out"] = self.build_lib
37+
38+
# Ensure that the associated packages can always be found locally
39+
for wrapper in build_ext.wrappers:
40+
pkgdir = wrapper.package_name.split(".")
41+
init_py = abspath(join(self.build_lib, *pkgdir, "__init__.py"))
42+
if exists(init_py):
43+
data["mapping"][wrapper.package_name] = init_py
44+
45+
# Ensure that the built extension can always be found
46+
build_ext.resolve_libs()
47+
for ext in build_ext.extensions:
48+
fname = build_ext.get_ext_filename(ext.name)
49+
data["mapping"][ext.name] = abspath(join(self.build_lib, fname))
50+
51+
rpybuild_libs = getattr(ext, "rpybuild_libs", None)
52+
if rpybuild_libs:
53+
for pth, _ in rpybuild_libs.values():
54+
dyld_path.add(dirname(pth))
55+
56+
# OSX-specific
57+
if dyld_path:
58+
dyld_path = ":".join(dyld_path)
59+
if "DYLD_LIBRARY_PATH" in env:
60+
dyld_path += ":" + env["DYLD_LIBRARY_PATH"]
61+
env["DYLD_LIBRARY_PATH"] = dyld_path
62+
63+
return data, env
64+
65+
66+
class _PackageFinder:
67+
"""
68+
Custom loader to allow loading built modules from their location
69+
in the build directory (as opposed to their install location)
70+
"""
71+
72+
# Set this to mapping returned from _BuiltEnv.setup_built_env
73+
mapping = {}
74+
75+
@classmethod
76+
def find_spec(cls, fullname, path, target=None):
77+
m = cls.mapping.get(fullname)
78+
if m:
79+
return importlib.util.spec_from_file_location(fullname, m)

robotpy_build/command/build_ext.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,27 @@ def _spawn(cmd):
158158
# Used in build_pyi
159159
ext.rpybuild_libs = libs
160160

161+
def resolve_libs(self):
162+
# used in _built_env
163+
platform = get_platform()
164+
if platform.os == "osx":
165+
for wrapper in self.wrappers:
166+
wrapper.finalize_extension()
167+
168+
from ..relink_libs import resolve_libs
169+
170+
install_root = get_install_root(self)
171+
172+
for ext in self.extensions:
173+
libs = resolve_libs(
174+
install_root,
175+
ext.rpybuild_wrapper,
176+
self.rpybuild_pkgcfg,
177+
)
178+
179+
# Used in build_pyi
180+
ext.rpybuild_libs = libs
181+
161182
def run(self):
162183
# files need to be generated before building can occur
163184
self.run_command("build_gen")

robotpy_build/command/build_pyi.py

Lines changed: 12 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
1-
import importlib.util
21
import json
32
import os
4-
from os.path import abspath, exists, dirname, join
3+
from os.path import exists, dirname, join
54
import subprocess
65
import sys
76

87
import pybind11_stubgen
9-
from setuptools import Command
10-
from distutils.errors import DistutilsError
118

12-
from .util import get_install_root
9+
try:
10+
from setuptools.errors import BaseError
11+
except ImportError:
12+
from distutils.errors import DistutilsError as BaseError
1313

14+
from ._built_env import _BuiltEnv, _PackageFinder
1415

15-
class GeneratePyiError(DistutilsError):
16+
17+
class GeneratePyiError(BaseError):
1618
pass
1719

1820

19-
class BuildPyi(Command):
21+
class BuildPyi(_BuiltEnv):
2022
base_package: str
2123

2224
command_name = "build_pyi"
2325
description = "Generates pyi files from built extensions"
2426

25-
user_options = [("build-lib=", "d", 'directory to "build" (copy) to')]
26-
27-
def initialize_options(self):
28-
self.build_lib = None
29-
30-
def finalize_options(self):
31-
self.set_undefined_options("build", ("build_lib", "build_lib"))
32-
3327
def run(self):
3428
# cannot build pyi files when cross-compiling
3529
if (
@@ -40,50 +34,18 @@ def run(self):
4034
return
4135

4236
# Gather information for needed stubs
43-
data = {"mapping": {}, "stubs": []}
44-
45-
# OSX-specific: need to set DYLD_LIBRARY_PATH otherwise modules don't
46-
# work. Luckily, that information was computed when building the
47-
# extensions...
48-
env = os.environ.copy()
49-
dyld_path = set()
50-
51-
# Requires information from build_ext to work
52-
build_ext = self.distribution.get_command_obj("build_ext")
53-
if build_ext.inplace:
54-
data["out"] = get_install_root(self)
55-
else:
56-
data["out"] = self.build_lib
57-
58-
# Ensure that the associated packages can always be found locally
59-
for wrapper in build_ext.wrappers:
60-
pkgdir = wrapper.package_name.split(".")
61-
init_py = abspath(join(self.build_lib, *pkgdir, "__init__.py"))
62-
if exists(init_py):
63-
data["mapping"][wrapper.package_name] = init_py
37+
data, env = self.setup_built_env()
38+
data["stubs"] = []
6439

6540
# Ensure that the built extension can always be found
41+
build_ext = self.get_finalized_command("build_ext")
6642
for ext in build_ext.extensions:
67-
fname = build_ext.get_ext_filename(ext.name)
68-
data["mapping"][ext.name] = abspath(join(self.build_lib, fname))
6943
data["stubs"].append(ext.name)
7044

71-
rpybuild_libs = getattr(ext, "rpybuild_libs", None)
72-
if rpybuild_libs:
73-
for pth, _ in rpybuild_libs.values():
74-
dyld_path.add(dirname(pth))
75-
7645
# Don't do anything if nothing is needed
7746
if not data["stubs"]:
7847
return
7948

80-
# OSX-specific
81-
if dyld_path:
82-
dyld_path = ":".join(dyld_path)
83-
if "DYLD_LIBRARY_PATH" in env:
84-
dyld_path += ":" + env["DYLD_LIBRARY_PATH"]
85-
env["DYLD_LIBRARY_PATH"] = dyld_path
86-
8749
data_json = json.dumps(data)
8850

8951
# Execute in a subprocess in case it crashes
@@ -101,45 +63,6 @@ def run(self):
10163
pass
10264

10365

104-
class _PackageFinder:
105-
"""
106-
Custom loader to allow loading built modules from their location
107-
in the build directory (as opposed to their install location)
108-
"""
109-
110-
mapping = {}
111-
112-
@classmethod
113-
def find_spec(cls, fullname, path, target=None):
114-
m = cls.mapping.get(fullname)
115-
if m:
116-
return importlib.util.spec_from_file_location(fullname, m)
117-
118-
119-
def generate_pyi(module_name: str, pyi_filename: str):
120-
print("generating", pyi_filename)
121-
122-
pybind11_stubgen.FunctionSignature.n_invalid_signatures = 0
123-
module = pybind11_stubgen.ModuleStubsGenerator(module_name)
124-
module.parse()
125-
if pybind11_stubgen.FunctionSignature.n_invalid_signatures > 0:
126-
print("FAILED to generate pyi for", module_name, file=sys.stderr)
127-
return False
128-
129-
module.write_setup_py = False
130-
with open(pyi_filename, "w") as fp:
131-
fp.write("#\n# AUTOMATICALLY GENERATED FILE, DO NOT EDIT!\n#\n\n")
132-
fp.write("\n".join(module.to_lines()))
133-
134-
typed = join(dirname(pyi_filename), "py.typed")
135-
print("generating", typed)
136-
if not exists(typed):
137-
with open(typed, "w") as fp:
138-
pass
139-
140-
return True
141-
142-
14366
def main():
14467
cfg = json.load(sys.stdin)
14568

robotpy_build/command/update_init.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import json
2+
import os
3+
import subprocess
4+
import sys
5+
import typing
6+
7+
try:
8+
from setuptools.errors import BaseError
9+
except ImportError:
10+
from distutils.errors import DistutilsError as BaseError
11+
12+
from ._built_env import _BuiltEnv, _PackageFinder
13+
14+
15+
class UpdateInitError(BaseError):
16+
pass
17+
18+
19+
class UpdateInit(_BuiltEnv):
20+
update_list: typing.List[str]
21+
22+
command_name = "update_init"
23+
description = (
24+
"Updates __init__.py files using settings from tool.robotpy-build.update_init"
25+
)
26+
27+
def run(self):
28+
# cannot use when cross-compiling
29+
if (
30+
"_PYTHON_HOST_PLATFORM" in os.environ
31+
or "PYTHON_CROSSENV" in os.environ
32+
or not self.update_list
33+
):
34+
return
35+
36+
data, env = self.setup_built_env()
37+
data["update_list"] = self.update_list
38+
39+
data_json = json.dumps(data)
40+
41+
# Execute in a subprocess in case it crashes
42+
args = [sys.executable, "-m", __name__]
43+
try:
44+
subprocess.run(args, input=data_json.encode("utf-8"), env=env, check=True)
45+
except subprocess.CalledProcessError:
46+
raise UpdateInitError(
47+
"Failed to generate .pyi file (see above, or set RPYBUILD_SKIP_PYI=1 to ignore) via %s"
48+
% (args,)
49+
) from None
50+
51+
52+
def main():
53+
cfg = json.load(sys.stdin)
54+
55+
# Configure custom loader
56+
_PackageFinder.mapping = cfg["mapping"]
57+
sys.meta_path.insert(0, _PackageFinder)
58+
59+
from .. import tool
60+
61+
# Update init
62+
63+
for to_update in cfg["update_list"]:
64+
65+
sys.argv = ["<dummy>", "create-imports", "-w"] + to_update.split(" ", 1)
66+
67+
retval = tool.main()
68+
if retval != 0:
69+
break
70+
71+
72+
if __name__ == "__main__":
73+
main()

robotpy_build/config/pyproject_toml.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,14 @@ class RobotpyBuildConfig(Model):
416416
#: Python package to store version information and robotpy-build metadata in
417417
base_package: str
418418

419+
#: List of headers for the scan-headers tool to ignore
420+
scan_headers_ignore: List[str] = []
421+
422+
#: List of python packages with __init__.py to update when ``python setup.py update_init``
423+
#: is called -- this is an argument to the ``robotpy-build create-imports`` command, and
424+
#: may contain a space and the second argument to create-imports.
425+
update_init: List[str] = []
426+
419427
#:
420428
#: .. seealso:: :class:`.SupportedPlatform`
421429
#:

0 commit comments

Comments
 (0)