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.

  1. Everything that is inside an extension is private by default.

  2. Each [[python.module]] section in extension.toml file defines a public module.

  3. Everything imported in the public module that doesn’t start with underscore (_) is part of the public API.

  4. __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:

  1. For each extension it generates a config/python_api.md file that lists all the public API of extension.

  2. 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

  1. 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.

  1. 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.

  1. 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.