Extensions API
Extensions can provide API for other extensions to use. There are 3 types of extensions API:
C++ API
Python API
Settings
It is important to track the API of extensions to ensure that the API is stable and that the extensions are not breaking each other. Semantic versioning is used to express the compatibility of the API.
Major version: incompatible changes
Minor version: backward-compatible changes
Patch version: backward-compatible bug fixes
For extensions in Kit repo we use Kit version as semantic version, meaning that no API should break until Kit major version is incremented.
For extensions outside of Kit repo, we use the version specified in extension.toml
file.
Python API
In Python everything is public, any module is accessible from any other module. However, we want each extension to have a limited API surface that is exposed to other extensions. We introduce certain conventions to make it clear what is part of the public API and what is not.
Everything that is inside an extension is private by default.
Each
[[python.module]]
section inextension.toml
file defines a public module.Everything imported in the public module that doesn’t start with underscore (
_
) is part of the public API.__all__
variable in the public module can be used to limit the public API even further.
Example
[[python.module]]
name = "omni.foo"
omni/foo/__init__.py
:
__all__ = ["PublicClass", "public_function", "public_module"]
from .bar import PublicClass, public_function
from .hidden import HiddenClass, hidden_function, _underscore_function
from . import public_module
This way omni.foo
extension exposes PublicClass
, public_function
and public_module
to other extensions, but HiddenClass
and hidden_function
are private because they are not listed in __all__
.
If we remove __all__
then HiddenClass
and hidden_function
will be part of the public API, but _underscore_function
will still be private.
omni.foo.public_module
also became part of the public API and all the same rules apply recursively to it.
Tool to check API: repo_checkapi
To check the API of extensions, use the repo_checkapi
tool. Build Kit and run it as follows:
repo checkapi
It does 2 things:
For each extension it generates a
config/python_api.md
file that lists all the public API of extension.For each python file it checks all import statements and makes sure that all imported modules are part of the public API.
It runs on every MR on CI, but not as part of build. Whenever config/python_api.md
has changed, it would produce an error and ask to run the tool and commit the changes. This way developers are aware of any changes in the API, can review them and make sure that they are intentional.
Common API Check Failures
API doc for the following extensions was updated
Tool generates config/python_api.md
file for each extension and compares against the current version of that file. When they differ it prints:
API doc for the following extensions was updated. Run 'repo checkapi', review 'python_api.md' files and commit the changes.
Below that message there will be a list of extensions that have changed. Above you can find a diff for each extension in details.
To fix this, run repo checkapi
, review and commit the changes.
This way tool makes developers aware of any changes in the API, potentially allowing them to catch unintended or breaking changes before they are merged.
Using absolute import to import own private module:
If you have an extension omni.foo
and it has private modules omni/foo/impl_a.py
and omni/foo/impl_b.py
, you should not use absolute import to import them. E.g. in omni/foo/impl_a.py
you should not do from omni.foo.impl_b import B
, instead you should do from .impl_b import B
.
Importing public attribute from private module:
Let’s say extension omni.foo
exposes class A
in omni/foo/__init__.py
as such:
from .impl_a import A
That will fail the API check:
from omni.foo.impl_a import A
Correct way to import A
is:
from omni.foo import A
It is the same class, but impl_a
module is not part of the public API.
Debugging the tool
Tool scans for all python files it can find in the repo and resolves all imports in them. It has a few settings in repo_tools.toml
where to search and what to skip. It may fail to resolve some imports or types in some cases. To get more information about what is going wrong, run the tool with debug logging enabled:
repo -vv checkapi > checkapi.log
checkapi.log
will contain all the debug information about which modules were found and how all imports were resolved.
API Deprecation
It is a good practice to deprecate API before removing it. For instance, if you want to rename a function, you can deprecate the old function and add a new one. In the next major version Kit release, you can remove the old function.
To deprecate an API, use the @deprecated
decorator from omni.kit.app
module. It will print a warning when the deprecated API is used. The decorator can be used on classes, functions, and methods. Example:
@omni.kit.app.deprecated("Use bar instead")
def foo():
print("foo")
@omni.kit.app.deprecated("No biking")
class Bike:
@omni.kit.app.deprecated("No riding")
def ride(self):
pass
To print a warning the omni.kit.app.log_deprecation()
function is used. It prints it as a warning if developer warnings are enabled, otherwise it prints it as an info message. To enable developer use /app/enableDeveloperWarnings
setting, e.g. add --/app/enableDeveloperWarnings=1
command line argument.
In the public_api.md
file, the deprecated API will be marked with a [deprecated]
prefix.