-
Notifications
You must be signed in to change notification settings - Fork 160
Make sym and other genned pkgs namespace packages #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
63ad07b to
7f74579
Compare
b1e3d5a to
07f2d4f
Compare
aaron-skydio
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we had decided we were at least interested in a sym.gen package/module for generated functions instead of throwing user-generated functions into the top-level sym package - how does this relate to that?
| # ----------------------------------------------------------------------------- | ||
|
|
||
| from .buffer_func import buffer_func | ||
| __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore # mypy issue #1422 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LOL python/mypy#1422 (comment)
We should include the error type here, ideally also actually link to the issue instead of just commenting like this (so the reader doesn't have to figure out which repo exactly you're talking about)
Seems worth trying mypy 0.920 though if we haven't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pasted in the web address and made the type ignore specific to the actual error.
| __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore # mypy issue #1422 | ||
| {% if pkg_namespace == "sym" %} | ||
| from ._init import * | ||
| {% endif %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this should be the same template as __init__.py.jinja, with this bit conditioned on config.namespace_package? Not a strong opinion either way though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Maybe. The way I originally had it (with the complicated __init__.py) it definitely did make sense to have them be two different files, as one contained the exports, and the other contained the namespace package logic, and both would be rendered for every portion of the package.
| """ | ||
| Python runtime geometry package. | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We definitely still want the sym module docstring to show up properly, should make sure this doesn't break that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. Forgot about this. I made sure to check that it was there when initially when I was using the more elaborate __init__.py that allowed re-exporting, but forgot to do that here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. Added it in
|
|
||
| epsilon = 2.220446049250313e-15 | ||
| __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore # mypy issue #1422 | ||
| from ._init import * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably worth a comment either here or in sym/_init.py with some mention of what this is doing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. I added a comment. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking to explain the from ._init import * since that's the non-standard part, although the comment that this is a namespace package is good too :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this sound?
# Re-export core geometry and camera types
from ._init import *There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was imagining something describing why we do this as opposed to what it's doing (since generally what it's doing should be clear from the code :) ), something like:
# Import `sym` types onto the top-level module. Needs to be in a separate `_init` module since
# namespace packages do not evaluate every copy of `__init__.py` , just the first discoveredThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'll adjust accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a preview, I am going with
# Import sym types into the top level of the package. Needs to be done in all portions of
# the package since namespace packages do not evaluate every copy of `__init__.py`, just
# the first discovered. Import must occur after __path__ has been modified to ensure the
# sym._init module can be located.Just a note, since the only files we are importing are the geo and cam cal classes, it's not necessary to have the import be from _init.py (this was only necessary if any portion of the package could add imports to the __init__.py). We could just do from .rot3 import Rot3 and the like directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I do think it might make sense to just add them back in here directly, although it might be a little confusing if other generated sym modules also have those imports? Not sure, I'm fine either way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could also go either way. Though, since it is already from ._init import *, absent a compelling argument otherwise, I'm inclined to keep it that way.
|
I see the mention that |
I kind of assumed we would. Though, to make the change we'd have to change a lot of the usages and such (and I didn't want to make this PR any more crowded, as it already contains relatively complicated changes). |
07f2d4f to
c07e7bd
Compare
855d587 to
52bacd0
Compare
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__`.
52bacd0 to
5e6ce26
Compare
aaron-skydio
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm ready to approve this, the main remaining question is whether we should have a better sense of what's feasible as a follow-up before merging this to reduce any risk of churn (e.g. if it's not possible to make sym.gen a thing, maybe we'd rather "only allow symforce itself to generate things into the sym module", and then this wouldn't be necessary?)
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,
symand the other generated packages were not namespace packages. This caused issues when generated python packages in thesymnamespace attempted to access, for example,sym.Rot3(which they might do if the generated function returns asym.Rot3). Since the generated package itself was namedsym, butRot3was not defined locally, anAttributeErrorwould be raised.While an alternative would have been to instead not use the name
symfor generated packages (using, say,sym_geninstead), for reasons I didn't fully look into, we still want to generate our code within thesymnamespace (generating intosym.genwas 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
symclass in a package namedsym, we intend to soon add asym.utilpackage 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__.pyfiles 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
However, since the sub-modules can be created dynamically, it is impossible to re-export all names in this manner, as the first
__init__.pythat 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 whatcodegen_util.load_generated_functionis 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
sympackage, as we still wish to be able to doHowever, because all sub-modules we wish to export from the
sympackage are known at code-gen time, allowing this is not difficult. This only applies to names in the coresympackage, and any additional user generated code in thesympackage 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_packagefield of theirPythonConfigtoFalse. This is useful in our testing as it is the generated code being tested that is imported, not, for example, the checked insympackage code which is being imported.As a last note, I believe
pkgutil.extend_pathonly checks for portions of the package on thesys.path, and doesn't check for any portions than can only be found by finders on thesys.meta_path(for example,symforceitself is found by a finder on thesys.meta_pathbut not on thesys.pathduring an editable pip install). I don't expect this lapse to pose a problem, and addressing it immediately might just make the__init__.pys 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'ssubmodule_search_locationsif found to the__path__.