-
Notifications
You must be signed in to change notification settings - Fork 7
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
ADR for Public Python API Conventions #186
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
16. Python Public API Conventions | ||
================================= | ||
|
||
Context | ||
-------- | ||
|
||
OEP-49 lays out conventions for having `per-app API modules <https://docs.openedx.org/projects/openedx-proposals/en/latest/best-practices/oep-0049-django-app-patterns.html#api-py>`_, and we follow this module naming convention for apps in ``openedx_learning``. Unfortunately, this has a couple of drawbacks for Learning Core: | ||
|
||
#. **Casual API consumers will have difficulty finding functions they need.** Learning Core apps tend to be smaller than what OEP-49 envisioned, meaning that we have more of them. This makes them easier to reason about individually during development, but cumbersome to casually scan through for the things you need. This will get worse over time, as we continue to add new apps. | ||
#. **We will have less flexibility to refactor apps over time if API consumers import directly from each app.** We want to provide a more stable interface, but this repo will be undergoing heavy development in the coming years. | ||
|
||
For these reasons, we should supplement OEP-49 recommendations with a more consumer-friendly API layer. | ||
|
||
Decision | ||
-------- | ||
|
||
In addition to OEP-49, we will adopt the following practices: | ||
|
||
Learning Core Django apps will be grouped into packages. | ||
Apps in ``openedx_learning`` will be grouped into broadly related packages under ``openedx_learning.apps``. The first of these groups will be "authoring" (``openedx_learning.apps.authoring``). Future packages may include "learner", "personalization", "activity", "grading", etc. | ||
|
||
Learning Core Django apps will continue to have their own ``api`` modules. | ||
So for example, ``openedx_learning.apps.authoring.components.api`` will continue to exist. | ||
|
||
Learning Core will have a top level package for its public API. | ||
All public APIs intended for use by consumers of Learning Core will be represented as modules in the ``openedx_learning.api`` package that corresponds to the app groupings (e.g. ``openedx_learning.api.authoring``). | ||
|
||
App ``api`` modules will define their public functions using ``__all__``. | ||
The public API modules will do a wildcard import from the various apps in their package group. So ``openedx_learning/api/authoring.py`` might look like:: | ||
|
||
from ..apps.authoring.components.api import * | ||
from ..apps.authoring.contents.api import * | ||
from ..apps.authoring.publishing.api import * | ||
|
||
This relies on the individual apps to properly set ``__all__`` to the list of functions that they are willing to publicly support. | ||
|
||
App ``api`` modules within a package of apps still import from each other. | ||
So for example, ``openedx_learning.apps.authoring.components.api`` will continue to import APIs that it needs from ``..publishing.api``, instead of using the public API at ``openedx_learning.api.authoring``. These imports should not use wildcards. | ||
|
||
Functions and constants that are not listed as part of a module's ``__all__`` may still be imported by other app APIs in the same package grouping. This should allow a package more flexibility to create provisional APIs that we may not want to support publicly. | ||
|
||
If a function or attribute is intended to be completely private to an app's ``api`` module (i.e. not used even by other apps in its package), it should be prefixed with an underscore. | ||
|
||
App ``api`` modules should not import directly from apps outside their package. | ||
For example, ``openedx_learning.apps.personalization.api`` should import authoring API functions from ``openedx_learning.api.authoring``, **not** directly from something like ``openedx_learning.apps.authoring.components.api``. This will help to limit the impact of refactoring app package internal changes, as well as exposing shortcomings in the existing public APIs. | ||
|
||
Public API modules may implement their own functions. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this. Not only for convenience but also as a way to address the natural evolution of the API. Do you think this might replace versioning by creating shims and deprecating functions with good documentation and timelines? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if it will replace versioning altogether, but I definitely plan to use compatibility shims more liberally here than we have in for Django apps historically, and to make deprecations happen at the level of individual functions with deprecation warnings. |
||
In addition to aggregating app ``api`` modules via wildcard imports, public API modules like ``openedx_learning.api.authoring`` may implement their own functionality. This will be useful for convenience functions that invoke multiple app APIs, and for backwards compatibility shims. When possible, the bulk of the logic for these should continue to live in app-defined APIs, with the public API module acting more as a glue layer. | ||
|
||
Import Linter will be used to enforce these restrictions. | ||
|
||
Rejected Alternatives | ||
--------------------- | ||
|
||
Public APIs in each app package | ||
|
||
We could have added these aggregations as ``api`` modules in each app group package, e.g. ``openedx_learning.apps.authoring.api``. We are not doing this because: | ||
|
||
* It's more convenient for browsing and documentation generation to have the public API modules in the same package. | ||
* It's more idiomatic for Python libraries to expose their APIs in appropriately named modules (like ``authoring``), rather than all imported modules being named ``api``. | ||
|
||
Collapse the namespace to remove ``apps``. | ||
|
||
The ``openedx_learning`` package only has a few sub-packages inside: ``api``, | ||
``apps``, and ``lib``. We could remove the ``apps`` package and have those app package groups appear as peers of ``api`` and ``lib``, so something like:: | ||
|
||
# Public API package | ||
openedx_learning.api | ||
|
||
# All the app groups | ||
openedx_learning.authoring | ||
openedx_learning.grading | ||
openedx_learning.personalization | ||
|
||
# Public lib package | ||
openedx_learning.lib | ||
|
||
The reasons why we're not doing this: | ||
|
||
* Having an ``apps`` package makes it more obvious that all Django apps go in there, and that we should not have them in ``openedx_learning.lib`` or ``openedx_learning.api``. | ||
* It makes it easier to make blanket statements and lint checking like, "nothing in edx-platform should ever import from ``openedx_learning.apps``". | ||
* It's more consistent to keep all the app groups in one package, instead of having ``openedx_learning`` hold mostly app groups and a few odd exceptions like ``lib``. | ||
|
||
Use ``_api`` modules in apps instead of ``api``. | ||
|
||
To better signal that individual app ``api`` modules should not be used by consumers like ``edx-platform``, we could adopt the convention that apps create a ``_api`` module instead. | ||
|
||
I'm not adding this because it feels unintuitive that apps would import a private module from other apps, e.g. the ``components`` app importing from ``openedx_learning.apps.publishing._api``. My hope is that documentation and import linter rules in edx-platform will be enough to make it clear that APIs should only be imported from ``openedx_learning.api``. | ||
|
||
We should revisit this at a later time if this turns out to be a source of confusion. |
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.
Is it worth having a convention of
_api
to indicate more local APIs that are meant to be used internal to the project but is not reliable outside of theopenedx-learning
project? It seems cumbersom but also could help reduce the surface area of API that we have to guarantee to parties that build on top of openedx-learning.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.
That's an interesting idea. I guess it partly depends on where the maintenance boundaries end up being. This arrangement makes more sense to me if a single group of people is responsible for the broad package of
authoring
apps, as opposed to multiple teams working on separate groups of apps inauthoring
(which is... probable...?). I admit that part of my resistance to this idea might just be aesthetic–it feels weird to me that thecomponents
app would import from a private module ofpublishing
(openedx_learning.apps.publishing._api
).I'm hoping that between documentation and importlinter rules in edx-platform, it'll be clear that consumers should only import from modules in the
openedx_learning.api
package. But I'm definitely open to revisiting this if it turns out to be a source of confusion.