Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
- "py3.9-orjson"
- "py3.14-orjson"
- "py3.11-sphinxext"
- "doctest"
- "coverage_report"

- name: "macOS"
Expand All @@ -59,6 +60,7 @@ jobs:
tox-post-environments:
- "py3.11-sphinxext"
- "py3.14-orjson"
- "doctest"
- "coverage_report"

- name: "Windows"
Expand All @@ -70,6 +72,7 @@ jobs:
- "py3.9-mindeps"
- "py3.14-orjson"
- "py3.11-sphinxext"
- "doctest"
- "coverage_report"

- name: "Quality"
Expand Down
5 changes: 5 additions & 0 deletions changelog.d/20260702_015418_sirosen_doctest.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Development
-----------

- ``globus-sdk`` testing now leverages Sphinx ``doctest`` to make select
examples testable. (:pr:`NUMBER`)
4 changes: 2 additions & 2 deletions docs/authorization/scopes_and_consents/scopes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ strings. All scope objects support this by means of their defined
``__str__`` method. For example, the following is an example of
``str()`` and ``repr()`` usage:

.. code-block:: pycon
.. doctest::

>>> from globus_sdk.scopes import Scope
>>> foo = Scope("foo")
Expand All @@ -78,7 +78,7 @@ strings. All scope objects support this by means of their defined
>>> print(str(alpha))
alpha[*beta]
>>> print(repr(alpha))
Scope("alpha", dependencies=(Scope("beta", optional=True),))
Scope('alpha', dependencies=(Scope('beta', optional=True),))

Reference
~~~~~~~~~
Expand Down
20 changes: 20 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,32 @@
# merely because they are type annotated
autodoc_typehints_description_target = "documented_params"

# disable doctest on `>>>` blocks which are not annotated as `doctest` blocks
doctest_test_doctest_blocks = ""

doctest_global_setup = """\
import globus_sdk
from unittest import mock

UNDO_STACK = []

def sdk_doctest_patch(*args):
p = mock.patch(*args)
p.start()
UNDO_STACK.append(p.stop)
"""
doctest_global_cleanup = """\
for undo_callable in UNDO_STACK:
undo_callable()
"""


# sphinx extensions (minimally, we want autodoc and viewcode to build the site)
# plus, we have our own custom extension in the SDK to include
extensions = [
# sphinx-included extensions
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
# other packages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ We'll express that idea in a function which takes a :class:`datetime.timedelta`
as an ``offset``, an amount of time into the future.
This gives us a generic phrasing of getting a future date:

.. code-block:: python
.. testcode:: relative-deadline

import datetime

Expand All @@ -65,7 +65,7 @@ Creating a Task with the Deadline
Along with all of our other parameters to create the Transfer Task, here's
a sample task document with a deadline set for "an hour from now":

.. code-block:: python
.. testcode:: relative-deadline

# Globus Tutorial Collection 1
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
Expand Down
6 changes: 5 additions & 1 deletion src/globus_sdk/scopes/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ def serialize(

Example usage:

.. code-block:: pycon
.. testsetup::

from globus_sdk.scopes import ScopeParser, Scope

.. doctest::

>>> ScopeParser.serialize([Scope("foo"), "bar", Scope("qux")])
'foo bar qux'
Expand Down
15 changes: 11 additions & 4 deletions src/globus_sdk/services/gcs/connector_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,30 @@ class ConnectorTable:
It supports access by attribute or via a helper method for doing lookups.
For example, all of the following three usages retrieve the Azure Blob connector:

.. code-block:: pycon
.. testsetup:: connector-table

from globus_sdk import ConnectorTable

.. doctest:: connector-table

>>> ConnectorTable.AZURE_BLOB
GlobusConnectServerConnector(name='Azure Blob', connector_id='9436da0c-a444-11eb-af93-12704e0d6a4d')
>>> ConnectorTable.lookup("Azure Blob")
GlobusConnectServerConnector(name='Azure Blob', connector_id='9436da0c-a444-11eb-af93-12704e0d6a4d')
>>> ConnectorTable.lookup("9436da0c-a444-11eb-af93-12704e0d6a4d")
GlobusConnectServerConnector(name='Azure Blob', connector_id='9436da0c-a444-11eb-af93-12704e0d6a4d')

Given the results of such a lookup, you can retrieve the canonical name and ID for
a connector like so:

.. code-block:: pycon
.. doctest:: connector-table

>>> connector = ConnectorTable.AZURE_BLOB
>>> connector.name
'Azure Blob'
>>> connector.connector_id
'9436da0c-a444-11eb-af93-12704e0d6a4d'
"""
""" # noqa: E501

_connectors: t.ClassVar[tuple[tuple[str, str, str], ...]] = (
("ACTIVESCALE", "ActiveScale", "7251f6c8-93c9-11eb-95ba-12704e0d6a4d"),
Expand Down Expand Up @@ -129,7 +136,7 @@ def extend(

Usage example:

.. code-block:: pycon
.. doctest:: connector-table

>>> MyTable = ConnectorTable.extend(
... connector_name="Star Trek Transporter",
Expand Down
9 changes: 7 additions & 2 deletions src/globus_sdk/services/timers/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,14 @@ def create_timer(

.. tab-item:: Example Usage

.. code-block:: pycon
.. testsetup:: create-timer-example

>>> transfer_data = TransferData(...)
sdk_doctest_patch("globus_sdk.TransferData")
sdk_doctest_patch("globus_sdk.TimersClient")

.. doctest:: create-timer-example

>>> transfer_data = globus_sdk.TransferData(...)
>>> timers_client = globus_sdk.TimersClient(...)
>>> create_doc = globus_sdk.TransferTimer(
... name="my-timer",
Expand Down
49 changes: 32 additions & 17 deletions src/globus_sdk/services/timers/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,27 @@ class TransferTimer(GlobusPayload):

**Example Schedules**

.. testsetup:: schedules

from globus_sdk import OnceTimerSchedule, RecurringTimerSchedule

.. tab-set::

.. tab-item:: Run Once, Right Now

.. code-block:: python
.. testcode:: schedules

schedule = OnceTimerSchedule()

.. tab-item:: Run Once, At a Specific Time

.. code-block:: python
.. testcode:: schedules

schedule = OnceTimerSchedule(datetime="2023-09-22T00:00:00Z")

.. tab-item:: Run Every 5 Minutes, Until a Specific Time

.. code-block:: python
.. testcode:: schedules

schedule = RecurringTimerSchedule(
interval_seconds=300,
Expand All @@ -68,7 +72,7 @@ class TransferTimer(GlobusPayload):

.. tab-item:: Run Every 30 Minutes, 10 Times

.. code-block:: python
.. testcode:: schedules

schedule = RecurringTimerSchedule(
interval_seconds=1800,
Expand All @@ -77,20 +81,24 @@ class TransferTimer(GlobusPayload):

.. tab-item:: Run Every 10 Minutes, Indefinitely

.. code-block:: python
.. testcode:: schedules

schedule = RecurringTimerSchedule(interval_seconds=600)

Using these schedules, you can create a timer from a ``TransferData`` object:

.. code-block:: pycon
.. testsetup::

my_schedule = globus_sdk.OnceTimerSchedule()
sdk_doctest_patch("globus_sdk.TransferData")

.. doctest::

>>> from globus_sdk import TransferData, TransferTimer
>>> schedule = ...
>>> transfer_data = TransferData(...)
>>> timer = TransferTimer(
... name="my timer",
... schedule=schedule,
... schedule=my_schedule,
... body=transfer_data,
... )

Expand Down Expand Up @@ -149,23 +157,27 @@ class FlowTimer(GlobusPayload):

**Example Schedules**

.. testsetup:: schedules

from globus_sdk import OnceTimerSchedule, RecurringTimerSchedule

.. tab-set::

.. tab-item:: Run Once, Right Now

.. code-block:: python
.. testcode:: schedules

schedule = OnceTimerSchedule()

.. tab-item:: Run Once, At a Specific Time

.. code-block:: python
.. testcode:: schedules

schedule = OnceTimerSchedule(datetime="2023-09-22T00:00:00Z")

.. tab-item:: Run Every 5 Minutes, Until a Specific Time

.. code-block:: python
.. testcode:: schedules

schedule = RecurringTimerSchedule(
interval_seconds=300,
Expand All @@ -174,7 +186,7 @@ class FlowTimer(GlobusPayload):

.. tab-item:: Run Every 30 Minutes, 10 Times

.. code-block:: python
.. testcode:: schedules

schedule = RecurringTimerSchedule(
interval_seconds=1800,
Expand All @@ -183,20 +195,23 @@ class FlowTimer(GlobusPayload):

.. tab-item:: Run Every 10 Minutes, Indefinitely

.. code-block:: python
.. testcode:: schedules

schedule = RecurringTimerSchedule(interval_seconds=600)
my_schedule = RecurringTimerSchedule(interval_seconds=600)

Using these schedules, you can create a timer:

.. code-block:: pycon
.. testsetup::

my_schedule = globus_sdk.OnceTimerSchedule()

.. doctest::

>>> from globus_sdk import FlowTimer
>>> schedule = ...
>>> timer = FlowTimer(
... name="my timer",
... flow_id="00000000-19a9-44e6-9c1a-867da59d84ab",
... schedule=schedule,
... schedule=my_schedule,
... body={
... "body": {
... "input_key": "input_value",
Expand Down
4 changes: 4 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ globus_sdk_rmtree = docs/_build
changedir = docs/
commands = sphinx-build -j auto -d _build/doctrees -b html -W . _build/html {posargs}

[testenv:doctest]
base = docs
commands = sphinx-build -j auto -d _build/doctrees -b doctest -W . _build/html {posargs}

[testenv:twine-check]
skip_install = true
deps = build
Expand Down
Loading