Skip to content

Commit 855d587

Browse files
Make sym and other genned pkgs namespace packages
Namespace packages are packages whose `__path__` (which tells the python import system to look for sub-packages and sub-modules) has multiple directories, meaning it can have portions spread out across multiple directories. Previously, `sym` and the other generated packages were not namespace packages. This caused issues when generated python packages in the `sym` namespace attempted to access, for example, `sym.Rot3` (which they might do if the generated function returns a `sym.Rot3`). Since the generated package itself was named `sym`, but `Rot3` was not defined locally, an `AttributeError` would be raised. While an alternative would have been to instead not use the name `sym` for generated packages (using, say, `sym_gen` instead), for reasons I didn't fully look into, we still want to generate our code within the `sym` namespace (generating into `sym.gen` was considered as an option but to do so would require the changes in this commit regardless). While currently we haven't needed to generate any functions returning a `sym` class in a package named `sym`, we intend to soon add a `sym.util` package which will be used in a lot of places. That can't be done until this namespace conflict is resolved. Note, while a python2 compatible namespace package has multiple `__init__.py` files for the top-level package spread across different locations, only one of them will be executed. This makes it difficult to re-export the contents of sub-modules into the top-level namespace. The normal way to re-export a name is to write ``` python3 from .sub_module import name ``` However, since the sub-modules can be created dynamically, it is impossible to re-export all names in this manner, as the first `__init__.py` that is created has no way of knowing what names one might want to re-export from subsequent modules. It is possible to put all names one wishes to export in a standard file, say `_init.py`, then dynamically search for such files and execute their contents, but we considered the additional complexity to be too large of a burden (as users would have a harder time understand their generated code, and this would give future maintainers a hard time). And so, we decided to simply stop re-exporting any names in the `__init__.py`'s of generated code (kind of in the style of pep 420 python3 packages). This makes loading a generated function more difficult if one uses `codegen_util.load_generated_package`, as now simply importing a generated package won't give you access to any of the package's contents. However, this is what `codegen_util.load_generated_function` is for, so hopefully the user experience shouldn't be too negatively impacted. The one exception to the general ban of re-exporting names is the `sym` package, as we still wish to be able to do ``` python3 import sym sym.Rot3.identity() ``` However, because all sub-modules we wish to export from the `sym` package are known at code-gen time, allowing this is not difficult. This only applies to names in the core `sym` package, and any additional user generated code in the `sym` package will not be re-rexported in the top-level namespace. A user can prevent their package from being generated as a namespace package by setting the `namespace_package` field of their `PythonConfig` to `False`. This is useful in our testing as it is the generated code being tested that is imported, not, for example, the checked in `sym` package code which is being imported. As a last note, I believe `pkgutil.extend_path` only checks for portions of the package on the `sys.path`, and doesn't check for any portions than can only be found by finders on the `sys.meta_path` (for example, `symforce` itself is found by a finder on the `sys.meta_path` but not on the `sys.path` during an editable pip install). I don't expect this lapse to pose a problem, and addressing it immediately might just make the `__init__.py`s look more complicated than they need to be, but if this does become a problem, know that the situation can be partially addressed trying to find the spec using the finders, and adding the spec's `submodule_search_locations` if found to the `__path__`.
1 parent 7f74579 commit 855d587

File tree

18 files changed

+132
-64
lines changed

18 files changed

+132
-64
lines changed

gen/python/sym/__init__.py

Lines changed: 5 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/python/sym/_init.py

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

notebooks/tutorials/codegen_tutorial.ipynb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -390,16 +390,16 @@
390390
"params.L = [0.5, 0.3]\n",
391391
"params.m = [0.3, 0.2]\n",
392392
"\n",
393-
"gen_module = codegen_util.load_generated_package(\n",
394-
" namespace, double_pendulum_python_data.function_dir\n",
393+
"gen_double_pendulum = codegen_util.load_generated_function(\n",
394+
" \"double_pendulum\", double_pendulum_python_data.function_dir\n",
395395
")\n",
396-
"gen_module.double_pendulum(ang, dang, consts, params)"
396+
"gen_double_pendulum(ang, dang, consts, params)"
397397
]
398398
}
399399
],
400400
"metadata": {
401401
"kernelspec": {
402-
"display_name": "Python 3",
402+
"display_name": "Python 3 (ipykernel)",
403403
"language": "python",
404404
"name": "python3"
405405
},
@@ -413,7 +413,7 @@
413413
"name": "python",
414414
"nbconvert_exporter": "python",
415415
"pygments_lexer": "ipython3",
416-
"version": "3.8.9"
416+
"version": "3.8.14"
417417
}
418418
},
419419
"nbformat": 4,

symforce/codegen/backends/python/python_config.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ class PythonConfig(CodegenConfig):
3333
times.
3434
reshape_vectors: Allow rank 1 ndarrays to be passed in for row and column vectors by
3535
automatically reshaping the input.
36+
namespace_package: Generate the package as a namespace package, meaning it can be split
37+
across multiple directories.
3638
"""
3739

3840
doc_comment_line_prefix: str = ""
3941
line_length: int = 100
4042
use_eigen_types: bool = True
4143
use_numba: bool = False
4244
reshape_vectors: bool = True
45+
namespace_package: bool = True
4346

4447
@classmethod
4548
def backend_name(cls) -> str:
@@ -50,10 +53,11 @@ def template_dir(cls) -> Path:
5053
return CURRENT_DIR / "templates"
5154

5255
def templates_to_render(self, generated_file_name: str) -> T.List[T.Tuple[str, str]]:
53-
return [
54-
("function/FUNCTION.py.jinja", f"{generated_file_name}.py"),
55-
("function/__init__.py.jinja", "__init__.py"),
56-
]
56+
templates = [("function/FUNCTION.py.jinja", f"{generated_file_name}.py")]
57+
if self.namespace_package:
58+
return templates + [("function/namespace_init.py.jinja", "__init__.py")]
59+
else:
60+
return templates + [("function/__init__.py.jinja", "__init__.py")]
5761

5862
def printer(self) -> CodePrinter:
5963
return python_code_printer.PythonCodePrinter()

symforce/codegen/backends/python/templates/function/__init__.py.jinja

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@
22
# SymForce - Copyright 2022, Skydio, Inc.
33
# This source code is under the Apache 2.0 license found in the LICENSE file.
44
# ---------------------------------------------------------------------------- #}
5-
from .{{ spec.name }} import {{ spec.name }}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{# ----------------------------------------------------------------------------
2+
# SymForce - Copyright 2022, Skydio, Inc.
3+
# This source code is under the Apache 2.0 license found in the LICENSE file.
4+
# ---------------------------------------------------------------------------- #}
5+
{% if pkg_namespace == "sym" %}
6+
"""
7+
Python runtime geometry package.
8+
"""
9+
10+
{% endif %}
11+
# Make package a namespace package by adding other portions to the __path__
12+
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore[has-type]
13+
# https://github.com/python/mypy/issues/1422
14+
{% if pkg_namespace == "sym" %}
15+
from ._init import *
16+
{% endif %}

symforce/codegen/cam_package_codegen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ def generate(config: CodegenConfig, output_dir: str = None) -> str:
293293
all_types=list(geo_package_codegen.DEFAULT_GEO_TYPES) + list(DEFAULT_CAM_TYPES),
294294
numeric_epsilon=sf.numeric_epsilon,
295295
),
296-
output_path=cam_package_dir / "__init__.py",
296+
output_path=cam_package_dir
297+
/ ("_init.py" if config.namespace_package else "__init__.py"),
297298
)
298299

299300
for name in ("cam_package_python_test.py",):

symforce/codegen/codegen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def generate_function(
484484
# Namespace of this function + generated types
485485
self.namespace = namespace
486486

487-
template_data = dict(self.common_data(), spec=self)
487+
template_data = dict(self.common_data(), spec=self, pkg_namespace=namespace)
488488
template_dir = self.config.template_dir()
489489

490490
backend_name = self.config.backend_name()

symforce/codegen/geo_package_codegen.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,20 @@ def generate(config: CodegenConfig, output_dir: str = None) -> str:
217217
)
218218

219219
# Package init
220+
if config.namespace_package:
221+
templates.add(
222+
template_path=Path("function", "namespace_init.py.jinja"),
223+
data=dict(pkg_namespace="sym"),
224+
output_path=package_dir / "__init__.py",
225+
)
220226
templates.add(
221227
template_path=Path("geo_package", "__init__.py.jinja"),
222228
data=dict(
223229
Codegen.common_data(),
224230
all_types=DEFAULT_GEO_TYPES,
225231
numeric_epsilon=sf.numeric_epsilon,
226232
),
227-
output_path=package_dir / "__init__.py",
233+
output_path=package_dir / ("_init.py" if config.namespace_package else "__init__.py"),
228234
)
229235

230236
# Test example

symforce/opt/numeric_factor.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,7 @@ def from_file_python(
7575
"""
7676
assert all(opt_key in keys for opt_key in optimized_keys)
7777
function_dir = Path(output_dir) / "python" / "symforce" / namespace
78-
linearization_function = getattr(
79-
codegen_util.load_generated_package(f"{namespace}.{name}", function_dir),
80-
name,
81-
)
78+
linearization_function = codegen_util.load_generated_function(name, function_dir)
8279
return cls(
8380
keys=keys, optimized_keys=optimized_keys, linearization_function=linearization_function
8481
)

0 commit comments

Comments
 (0)