Skip to content
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

docs: add linkcode extension #261

Merged
merged 6 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ Change Log
Unreleased
----------

[2.0.1] - 2025-02-18
--------------------

Changed
~~~~~~~

* Added linkcode Sphinx extension to the documentation.
* Added common_refs.rst file to reuse common references in the documentation.

[2.0.0] - 2025-02-17
---------------------

Expand Down
8 changes: 8 additions & 0 deletions docs/common_refs.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _Tutor: https://docs.tutor.edly.io/

.. Replaces
.. |PipelineStep| replace:: :class:`PipelineStep <openedx_filters.filters.PipelineStep>`
.. |PipelineStep.run_filter| replace:: :class:`run_filter <openedx_filters.filters.PipelineStep.run_filter>`
.. |CourseEnrollmentStarted| replace:: :class:`CourseEnrollmentStarted <openedx_filters.learning.filters.CourseEnrollmentStarted>`
.. |OpenEdxPublicFilter| replace:: :class:`OpenEdxPublicFilter <openedx_filters.tooling.OpenEdxPublicFilter>`
.. |OpenEdxPublicFilter.run_pipeline| replace:: :class:`run_pipeline <openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline>`
13 changes: 7 additions & 6 deletions docs/concepts/openedx-filters.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. include:: ../common_refs.rst

Open edX Filters
================

Expand All @@ -18,7 +20,7 @@ How do Open edX Filters work?

Open edX Filters are implemented using an accumulative pipeline mechanism, which executes a series of functions in a specific order. Each function in the pipeline receives the output of the previous function as input, allowing developers to build complex processing logic by chaining multiple functions together. The pipeline ensures that the order of execution is maintained and that the result of a previous function is available to the current one in the form of a pipeline.

This pipeline mechanism is implemented by the `OpenEdxPublicFilter`_ class, which provides the necessary tools to fulfill the Open edX Filters requirements mentioned previously, such as ordered execution, configurability, interchangeable functions, argument definition, and cumulative behavior. This enables filters to modify the flow of the application dynamically during runtime based on predefined business logic or conditions. You can review the :doc:`Open edX Filters Tooling <../reference/filters-tooling>` for more information on the available methods and classes.
This pipeline mechanism is implemented by the |OpenEdxPublicFilter| class, which provides the necessary tools to fulfill the Open edX Filters requirements mentioned previously, such as ordered execution, configurability, interchangeable functions, argument definition, and cumulative behavior. This enables filters to modify the flow of the application dynamically during runtime based on predefined business logic or conditions. You can review the :doc:`Open edX Filters Tooling <../reference/filters-tooling>` for more information on the available methods and classes.

Architectural Diagram
*********************
Expand All @@ -33,7 +35,7 @@ Components
~~~~~~~~~~

#. Application (caller): The component that calls the filter during its execution, triggering the pipeline to process the input data. Developers may have added this call to a part of the application to include different behaviors. E.g., a user enrolls in a course, triggering the `CourseEnrollmentStarted filter`_.
#. OpenEdxPublicFilter: The class that implements all methods used to manage the execution of the filter.
#. |OpenEdxPublicFilter|: The class that implements all methods used to manage the execution of the filter.
#. PipelineStep1...N: The pipeline steps that are executed in sequence, each processing the input data and returning potentially modified data. These steps are defined by the developer to introduce additional behaviors. E.g., a pipeline step that checks user eligibility for enrollment.

Workflow
Expand All @@ -43,7 +45,7 @@ Workflow

#. The caller passes the input data to the filter through the ``run_filter`` method, this data are in-memory platform objects that the filter will process.

#. The ``run_filter`` method of the filter calls the ``OpenEdxPublicFilter.run_pipeline`` method under the hood, which manages the execution of the filter's pipeline.
#. The ``run_filter`` method of the filter calls the |OpenEdxPublicFilter.run_pipeline| method under the hood, which manages the execution of the filter's pipeline.

#. This method retrieves the configuration from ``OPEN_EDX_FILTERS_CONFIG``, which defines a list of N functions :math:`f_1, f_2, \ldots, f_{n}` that will be executed.

Expand All @@ -53,7 +55,7 @@ Workflow

#. Each subsequent function receives the output from the previous function and returns its modified output until all functions have been executed.

#. At any point in the pipeline, a developer can halt execution by raising an exception, based on conditions defined in the processing logic, to stop the application flow. Let's assume that :math:`f_{2}` raises an exception instead of returning the modified arguments ``kwargs_2``. In this case, the pipeline stops, and the ``OpenEdxPublicFilter.run_pipeline`` method raises the exception to the caller as the final output. From there the caller can handle the exception as needed.
#. At any point in the pipeline, a developer can halt execution by raising an exception, based on conditions defined in the processing logic, to stop the application flow. Let's assume that :math:`f_{2}` raises an exception instead of returning the modified arguments ``kwargs_2``. In this case, the pipeline stops, and the |OpenEdxPublicFilter.run_pipeline| method raises the exception to the caller as the final output. From there the caller can handle the exception as needed.

#. If no exceptions are raised, the pipeline continues executing the functions until the final function :math:`f_{n}` has been executed.

Expand All @@ -68,7 +70,7 @@ Here's an example of the `CourseEnrollmentStarted filter`_ in action:

#. A user enrolls in a course, triggering the `CourseEnrollmentStarted filter`_ by calling the ``run_filter`` method with the enrollment details. This filter processes information about the user, course, and enrollment details.

#. The ``run_pipeline`` method executes a series of functions configured in ``OPEN_EDX_FILTERS_CONFIG``, e.g. checking user eligibility for enrollment or updating the enrollment status in a third-party system.
#. The |OpenEdxPublicFilter.run_pipeline| method executes a series of functions configured in ``OPEN_EDX_FILTERS_CONFIG``, e.g. checking user eligibility for enrollment or updating the enrollment status in a third-party system.

#. Each function can modify the input data or halt the process based on business logic, e.g. denying enrollment if the user is ineligible.

Expand All @@ -89,5 +91,4 @@ For more information on how to use Open edX Filters, refer to the :doc:`how-tos
.. _Django Signals Documentation: https://docs.djangoproject.com/en/4.2/topics/signals/
.. _CourseEnrollmentStarted filter: https://github.com/openedx/edx-platform/blob/master/common/djangoapps/student/models/course_enrollment.py#L719-L724
.. _Python Social Auth: https://python-social-auth.readthedocs.io/en/latest/pipeline.html
.. _OpenEdxPublicFilter: https://github.com/openedx/openedx-filters/blob/main/openedx_filters/tooling.py#L14-L15
.. _Open edX Django plugin: https://edx.readthedocs.io/projects/edx-django-utils/en/latest/plugins/readme.html
76 changes: 76 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import inspect
import os
import re
import sys
from os.path import dirname, relpath

sys.path.insert(0, os.path.abspath('..'))

# -- Project information -----------------------------------------------------
Expand All @@ -39,6 +42,7 @@
'sphinx.ext.autosummary',
'sphinx.ext.intersphinx',
'sphinx.ext.napoleon',
'sphinx.ext.linkcode',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down Expand Up @@ -135,3 +139,75 @@
None,
),
}

# Linkcode Extension Configuration

REPO_URL = "https://github.com/openedx/openedx-filters/blob/main"

def linkcode_resolve(domain: str, info: dict[str, str]) -> str | None:
"""
Resolves source code links for Python objects in Sphinx documentation.
This function is based on the `linkcode_resolve` function in the SciPy project.

Args:
domain (str): The language domain of the object. Only processes Python objects ('py')
info (dict[str, str]): Dictionary containing information about the object to link.
Must contain:
- 'module': Name of the module containing the object
- 'fullname': Complete name of the object including its path

Returns:
str | None: URL to the source code on GitHub with specific line numbers,
or None if the link cannot be resolved
"""
if domain != "py":
return None

modname = info["module"]
fullname = info["fullname"]

submod = sys.modules.get(modname)
if submod is None:
return None

obj = submod
for part in fullname.split("."):
try:
obj = getattr(obj, part)
except Exception:
return None

# Use the original function object if it is wrapped.
while hasattr(obj, "__wrapped__"):
obj = obj.__wrapped__

# Get the file path where the object is defined
try:
# Try to get the file path of the object directly
file_path = inspect.getsourcefile(obj)
except Exception:
try:
# If that fails, try to get the file path of the module where the object is defined
file_path = inspect.getsourcefile(sys.modules[obj.__module__])
except Exception:
# If both attempts fail, set file_path to None
file_path = None
if not file_path:
return None

try:
source, start_line = inspect.getsourcelines(obj)
except Exception:
start_line = None

if start_line:
linespec = f"#L{start_line}-L{start_line + len(source) - 1}"
else:
linespec = ""

import openedx_filters

start_dir = os.path.abspath(os.path.join(dirname(openedx_filters.__file__), ".."))
file_path = relpath(file_path, start=start_dir).replace(os.path.sep, "/")

return f"{REPO_URL}/{file_path}{linespec}"
9 changes: 4 additions & 5 deletions docs/how-tos/create-a-new-filter.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. include:: ../common_refs.rst

Create a New Open edX Filter with Long-Term Support
####################################################

Expand Down Expand Up @@ -97,7 +99,7 @@ In our example, the filter arguments could include the user, course key, and enr
Step 5: Implement the Filter Definition
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Implement the :term:`filter definition` by creating a new class that inherits from the `OpenEdxPublicFilter`_ class. The filter definition should implement the ``run_filter`` method, which defines the input and output behavior of the filter. The ``run_filter`` method should call the method `run_pipeline`_, passing down the input arguments and returning the final output of the filter. This class should be placed in the appropriate subdomain module in the library, in the ``filters.py`` file.
Implement the :term:`filter definition` by creating a new class that inherits from the |OpenEdxPublicFilter| class. The filter definition should implement the ``run_filter`` method, which defines the input and output behavior of the filter. The ``run_filter`` method should call the method |OpenEdxPublicFilter.run_pipeline|, passing down the input arguments and returning the final output of the filter. This class should be placed in the appropriate subdomain module in the library, in the ``filters.py`` file.

.. note:: The input arguments of the ``run_filter`` method should match the arguments that the triggering logic provides. The output of the filter should be consistent with the behavior that the filter intends to modify. Usually, the output is the modified data or the original data if no modifications are needed.

Expand Down Expand Up @@ -138,7 +140,7 @@ In our example, the filter definition could be implemented as follows:

- The ``filter_type`` attribute should be set to the filter type that was identified in the previous steps. This attribute is used to identify the filter in the :term:`filter configuration`.
- The ``PreventEnrollment`` class is a custom exception that is raised when the filter should halt the application behavior.
- The ``run_filter`` method is the main method of the filter that is called when the filter is triggered. The method should call the ``run_pipeline`` method, passing down the input arguments and returning the final output of the filter.
- The ``run_filter`` method is the main method of the filter that is called when the filter is triggered. The method should call the |OpenEdxPublicFilter.run_pipeline| method, passing down the input arguments and returning the final output of the filter.
- Use arguments names that are consistent with the triggering logic to avoid confusion and improve readability.

.. note:: Implement exceptions that are related to the filter behavior and specify how the filter should modify the application behavior with each exception. The caller should handle each exception differently based the exceptions purpose. For example, the caller should halt the application behavior when the ``PreventEnrollment`` exception is raised.
Expand Down Expand Up @@ -179,8 +181,5 @@ After implementing the filter, you should continue the contribution process by c

For more details on how the contribution flow works, refer to the :doc:`docs.openedx.org:developers/concepts/hooks_extension_framework` documentation.

.. _Tutor: https://docs.tutor.edly.io/
.. _Add Extensibility Mechanism to IDV to Enable Integration of New IDV Vendor Persona: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/4307386369/Proposal+Add+Extensibility+Mechanisms+to+IDV+to+Enable+Integration+of+New+IDV+Vendor+Persona
.. _OpenEdxPublicFilter: https://github.com/openedx/openedx-filters/blob/main/openedx_filters/tooling.py#L14
.. _run_pipeline: https://github.com/openedx/openedx-filters/blob/main/openedx_filters/tooling.py#L164
.. _test_filters.py: https://github.com/openedx/edx-platform/blob/master/common/djangoapps/student/tests/test_filters.py#L114-L190
Loading