Skip to content

Commit 19e11c5

Browse files
saurabhrbSaurabh Badenkal
andauthored
Fix docstring type annotations for Microsoft Learn compatibility (microsoft#153)
## Summary Fix broken cross-references (`<xref:of>`, `<xref:mapping>`, `<xref:to>`) in the Microsoft Learn API reference docs caused by Sphinx-style `:type:` and `:rtype:` directives. ## Problem The Learn doc pipeline processes `:type:` and `:rtype:` Sphinx directives differently from standard Sphinx. Every word between `:class:` back-tick references is treated as a separate cross-reference. For example: ``` :rtype: :class:`list` of :class:`str` ``` Becomes: ```yaml types: - <xref:list> <xref:of> <xref:str> ``` There is no type `of` in the Learn cross-reference database, so it renders as a broken link on the published page. ## Root Cause This was introduced in commit f0e8987 ("sphinx doc string", 2025-11-17) which converted the original bracket-notation docstrings (`list[str]`, `dict or list[dict]`) to Sphinx-style `:class:` syntax. Later commits that added new APIs (operation namespaces, dataframe, etc.) perpetuated the same broken pattern. ## Fix Replaced all 42 occurrences across 6 source files with Python bracket notation that the Learn pipeline handles correctly: | Before (broken) | After (correct) | |---|---| | `:class:\`list\` of :class:\`str\`` | `list[str]` | | `:class:\`dict\` or :class:\`list\` of :class:\`dict\`` | `dict or list[dict]` | | `:class:\`collections.abc.Iterable\` of :class:\`list\` of :class:\`dict\`` | `collections.abc.Iterable[list[dict]]` | | `:class:\`dict\` mapping :class:\`str\` to :class:\`typing.Any\`` | `dict[str, typing.Any]` | ### Files changed **Docstring fixes:** - `src/PowerPlatform/Dataverse/client.py` (14 occurrences) - `src/PowerPlatform/Dataverse/operations/records.py` (14 occurrences) - `src/PowerPlatform/Dataverse/operations/tables.py` (7 occurrences) - `src/PowerPlatform/Dataverse/operations/dataframe.py` (3 occurrences) - `src/PowerPlatform/Dataverse/operations/query.py` (1 occurrence) - `src/PowerPlatform/Dataverse/models/table_info.py` (3 occurrences) **Prevention guidelines:** - `.claude/skills/dataverse-sdk-dev/SKILL.md` -- added "Docstring Type Annotations (Microsoft Learn Compatibility)" section - `src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-dev/SKILL.md` -- same section (kept both copies in sync) ## Testing - All 398 unit tests pass - Verified zero remaining occurrences of the broken pattern via regex scan --------- Co-authored-by: Saurabh Badenkal <sbadenkal@microsoft.com>
1 parent eebee60 commit 19e11c5

9 files changed

Lines changed: 145 additions & 47 deletions

File tree

.claude/skills/dataverse-sdk-dev/SKILL.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,35 @@ Navigation property names are case-sensitive and must match the entity's `$metad
5454
9. **Document public APIs** - Add Sphinx-style docstrings with examples for public methods
5555
10. **Define __all__ in module files** - Each module declares its own exports via `__all__` (e.g., `errors.py` defines `__all__ = ["HttpError", ...]`). Package `__init__.py` files should not re-export or redefine another module's `__all__`; they use `__all__ = []` to indicate no star-import exports.
5656
11. **Run black before committing** - Always run `python -m black <changed files>` before committing. CI will reject unformatted code. Config is in `pyproject.toml` under `[tool.black]`.
57+
58+
### Docstring Type Annotations (Microsoft Learn Compatibility)
59+
60+
This SDK's API reference is published on Microsoft Learn. The Learn doc pipeline parses `:type:` and `:rtype:` directives differently from standard Sphinx -- every word between `:class:` references is treated as a separate cross-reference (`<xref:word>`). Using Sphinx-style `:class:\`list\` of :class:\`str\`` produces broken `<xref:of>` links on Learn.
61+
62+
**Rules for `:type:` and `:rtype:` directives:**
63+
64+
- Use Python bracket notation for generic types: `list[str]`, `dict[str, typing.Any]`, `list[dict]`
65+
- Use `or` (without `:class:`) for union types: `str or None`, `dict or list[dict]`
66+
- Use bracket nesting for complex types: `collections.abc.Iterable[list[dict]]`
67+
- Use `~` prefix for SDK types to show short name: `list[~PowerPlatform.Dataverse.models.record.Record]`
68+
- `:class:` is fine for single standalone types: `:class:\`str\``, `:class:\`bool\``
69+
70+
**Never** use `:class:\`X\` of :class:\`Y\`` or `:class:\`X\` mapping :class:\`Y\` to :class:\`Z\`` -- the words `of`, `mapping`, `to` become broken `<xref:>` links.
71+
72+
**Correct examples:**
73+
74+
```rst
75+
:type data: dict or list[dict]
76+
:rtype: list[str]
77+
:rtype: collections.abc.Iterable[list[~PowerPlatform.Dataverse.models.record.Record]]
78+
:type select: list[str] or None
79+
:type columns: dict[str, typing.Any]
80+
```
81+
82+
**Wrong examples (NEVER use):**
83+
84+
```rst
85+
:type data: :class:`dict` or :class:`list` of :class:`dict`
86+
:rtype: :class:`list` of :class:`str`
87+
:type columns: :class:`dict` mapping :class:`str` to :class:`typing.Any`
88+
```

CONTRIBUTING.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,41 @@ published release:
121121
# After publishing v0.1.0b4, bump to v0.1.0b5 on main
122122
# Update version in pyproject.toml
123123
# Commit directly to main: "Bump version to 0.1.0b5 for next development cycle"
124+
```
125+
126+
### Docstring Type Annotations (Microsoft Learn Compatibility)
127+
128+
This SDK's API reference is published on [Microsoft Learn](https://learn.microsoft.com). The Learn doc pipeline processes `:type:` and `:rtype:` Sphinx directives differently from standard Sphinx -- every word between `:class:` back-tick references is treated as a separate cross-reference (`<xref:word>`). For example:
129+
130+
```
131+
:rtype: :class:`list` of :class:`str`
132+
```
133+
134+
This produces a broken `<xref:of>` link because `of` is not a valid type.
135+
136+
**Rules for `:type:` and `:rtype:` directives:**
137+
138+
- Use **Python bracket notation** for generic types: `list[str]`, `dict[str, typing.Any]`, `list[dict]`
139+
- Use **`or`** (without `:class:`) for union types: `str or None`, `dict or list[dict]`
140+
- Use **bracket nesting** for complex types: `collections.abc.Iterable[list[dict]]`
141+
- `:class:` is fine for **single standalone types**: `` :class:`str` ``, `` :class:`bool` ``
142+
143+
**NEVER** use the following patterns -- the connector words (`of`, `mapping`, `to`) become broken `<xref:>` links on Learn:
144+
145+
```
146+
:class:`X` of :class:`Y`
147+
:class:`X` mapping :class:`Y` to :class:`Z`
148+
```
149+
150+
Correct:
151+
```
152+
:type data: dict or list[dict]
153+
:rtype: list[str]
154+
:type select: list[str] or None
155+
```
156+
157+
Wrong:
158+
```
159+
:type data: :class:`dict` or :class:`list` of :class:`dict`
160+
:rtype: :class:`list` of :class:`str`
124161
```

src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-dev/SKILL.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,35 @@ This skill provides guidance for developers working on the PowerPlatform Dataver
2828
9. **Document public APIs** - Add Sphinx-style docstrings with examples for public methods
2929
10. **Define __all__ in module files** - Each module declares its own exports via `__all__` (e.g., `errors.py` defines `__all__ = ["HttpError", ...]`). Package `__init__.py` files should not re-export or redefine another module's `__all__`; they use `__all__ = []` to indicate no star-import exports.
3030
11. **Run black before committing** - Always run `python -m black <changed files>` before committing. CI will reject unformatted code. Config is in `pyproject.toml` under `[tool.black]`.
31+
32+
### Docstring Type Annotations (Microsoft Learn Compatibility)
33+
34+
This SDK's API reference is published on Microsoft Learn. The Learn doc pipeline parses `:type:` and `:rtype:` directives differently from standard Sphinx -- every word between `:class:` references is treated as a separate cross-reference (`<xref:word>`). Using Sphinx-style `:class:\`list\` of :class:\`str\`` produces broken `<xref:of>` links on Learn.
35+
36+
**Rules for `:type:` and `:rtype:` directives:**
37+
38+
- Use Python bracket notation for generic types: `list[str]`, `dict[str, typing.Any]`, `list[dict]`
39+
- Use `or` (without `:class:`) for union types: `str or None`, `dict or list[dict]`
40+
- Use bracket nesting for complex types: `collections.abc.Iterable[list[dict]]`
41+
- Use `~` prefix for SDK types to show short name: `list[~PowerPlatform.Dataverse.models.record.Record]`
42+
- `:class:` is fine for single standalone types: `:class:\`str\``, `:class:\`bool\``
43+
44+
**Never** use `:class:\`X\` of :class:\`Y\`` or `:class:\`X\` mapping :class:\`Y\` to :class:\`Z\`` -- the words `of`, `mapping`, `to` become broken `<xref:>` links.
45+
46+
**Correct examples:**
47+
48+
```rst
49+
:type data: dict or list[dict]
50+
:rtype: list[str]
51+
:rtype: collections.abc.Iterable[list[~PowerPlatform.Dataverse.models.record.Record]]
52+
:type select: list[str] or None
53+
:type columns: dict[str, typing.Any]
54+
```
55+
56+
**Wrong examples (NEVER use):**
57+
58+
```rst
59+
:type data: :class:`dict` or :class:`list` of :class:`dict`
60+
:rtype: :class:`list` of :class:`str`
61+
:type columns: :class:`dict` mapping :class:`str` to :class:`typing.Any`
62+
```

src/PowerPlatform/Dataverse/client.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,10 @@ def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dic
208208
:type table_schema_name: :class:`str`
209209
:param records: A single record dictionary or a list of record dictionaries.
210210
Each dictionary should contain column schema names as keys.
211-
:type records: :class:`dict` or :class:`list` of :class:`dict`
211+
:type records: dict or list[dict]
212212
213213
:return: List of created record GUIDs. Returns a single-element list for a single input.
214-
:rtype: :class:`list` of :class:`str`
214+
:rtype: list[str]
215215
216216
:raises TypeError: If ``records`` is not a dict or list[dict], or if the internal
217217
client returns an unexpected type.
@@ -260,12 +260,12 @@ def update(
260260
:param table_schema_name: Schema name of the table (e.g. ``"account"`` or ``"new_MyTestTable"``).
261261
:type table_schema_name: :class:`str`
262262
:param ids: Single GUID string or list of GUID strings to update.
263-
:type ids: :class:`str` or :class:`list` of :class:`str`
263+
:type ids: str or list[str]
264264
:param changes: Dictionary of changes for single/broadcast mode, or list of dictionaries
265265
for paired mode. When ``ids`` is a list and ``changes`` is a single dict,
266266
the same changes are broadcast to all records. When both are lists, they must
267267
have equal length for one-to-one mapping.
268-
:type changes: :class:`dict` or :class:`list` of :class:`dict`
268+
:type changes: dict or list[dict]
269269
270270
:raises TypeError: If ``ids`` is not str or list[str], or if ``changes`` type doesn't match usage pattern.
271271
@@ -312,7 +312,7 @@ def delete(
312312
:param table_schema_name: Schema name of the table (e.g. ``"account"`` or ``"new_MyTestTable"``).
313313
:type table_schema_name: :class:`str`
314314
:param ids: Single GUID string or list of GUID strings to delete.
315-
:type ids: :class:`str` or :class:`list` of :class:`str`
315+
:type ids: str or list[str]
316316
:param use_bulk_delete: When ``True`` (default) and ``ids`` is a list, execute the BulkDelete action and
317317
return its async job identifier. When ``False`` each record is deleted sequentially.
318318
:type use_bulk_delete: :class:`bool`
@@ -367,21 +367,21 @@ def get(
367367
:param record_id: Optional GUID to fetch a specific record. If None, queries multiple records.
368368
:type record_id: :class:`str` or None
369369
:param select: Optional list of attribute logical names to retrieve. Column names are case-insensitive and automatically lowercased (e.g. ``["new_Title", "new_Amount"]`` becomes ``"new_title,new_amount"``).
370-
:type select: :class:`list` of :class:`str` or None
370+
:type select: list[str] or None
371371
:param filter: Optional OData filter string, e.g. ``"name eq 'Contoso'"`` or ``"new_quantity gt 5"``. Column names in filter expressions must use exact lowercase logical names (e.g. ``"new_quantity"``, not ``"new_Quantity"``). The filter string is passed directly to the Dataverse Web API without transformation.
372372
:type filter: :class:`str` or None
373373
:param orderby: Optional list of attributes to sort by, e.g. ``["name asc", "createdon desc"]``. Column names are automatically lowercased.
374-
:type orderby: :class:`list` of :class:`str` or None
374+
:type orderby: list[str] or None
375375
:param top: Optional maximum number of records to return.
376376
:type top: :class:`int` or None
377377
:param expand: Optional list of navigation properties to expand, e.g. ``["primarycontactid"]``. Navigation property names are case-sensitive and must match the server-defined names exactly. These are NOT automatically transformed. Consult entity metadata for correct casing.
378-
:type expand: :class:`list` of :class:`str` or None
378+
:type expand: list[str] or None
379379
:param page_size: Optional number of records per page for pagination.
380380
:type page_size: :class:`int` or None
381381
382382
:return: Single record dict if ``record_id`` is provided, otherwise a generator
383383
yielding lists of record dictionaries (one list per page).
384-
:rtype: :class:`dict` or :class:`collections.abc.Iterable` of :class:`list` of :class:`dict`
384+
:rtype: dict or collections.abc.Iterable[list[dict]]
385385
386386
:raises TypeError: If ``record_id`` is provided but not a string.
387387
@@ -456,7 +456,7 @@ def query_sql(self, sql: str) -> List[Dict[str, Any]]:
456456
:type sql: :class:`str`
457457
458458
:return: List of result row dictionaries. Returns an empty list if no rows match.
459-
:rtype: :class:`list` of :class:`dict`
459+
:rtype: list[dict]
460460
461461
:raises ~PowerPlatform.Dataverse.core.errors.SQLParseError: If the SQL query uses unsupported syntax.
462462
:raises ~PowerPlatform.Dataverse.core.errors.HttpError: If the Web API returns an error.
@@ -545,7 +545,7 @@ class ItemStatus(IntEnum):
545545
1036: {"Active": "Actif", "Inactive": "Inactif"}
546546
}
547547
548-
:type columns: :class:`dict` mapping :class:`str` to :class:`typing.Any`
548+
:type columns: dict[str, typing.Any]
549549
:param solution_unique_name: Optional solution unique name that should own the new table. When omitted the table is created in the default solution.
550550
:type solution_unique_name: :class:`str` or None
551551
:param primary_column_schema_name: Optional primary name column schema name with customization prefix value (e.g. ``"new_MyTestTable"``). If not provided, defaults to ``"{customization prefix value}_Name"``.
@@ -634,7 +634,7 @@ def list_tables(self) -> list[dict[str, Any]]:
634634
List all non-private tables in the Dataverse environment.
635635
636636
:return: List of EntityDefinition metadata dictionaries.
637-
:rtype: :class:`list` of :class:`dict`
637+
:rtype: list[dict]
638638
639639
Example:
640640
List all non-private tables and print their logical names::
@@ -666,9 +666,9 @@ def create_columns(
666666
:param columns: Mapping of column schema names (with customization prefix value) to supported types. All custom column names must include the customization prefix value** (e.g. ``"new_Notes"``). Primitive types include
667667
``"string"`` (alias: ``"text"``), ``"int"`` (alias: ``"integer"``), ``"decimal"`` (alias: ``"money"``), ``"float"`` (alias: ``"double"``), ``"datetime"`` (alias: ``"date"``), ``"bool"`` (alias: ``"boolean"``), and ``"file"``. Enum subclasses (IntEnum preferred)
668668
generate a local option set and can specify localized labels via ``__labels__``.
669-
:type columns: :class:`dict` mapping :class:`str` to :class:`typing.Any`
669+
:type columns: dict[str, typing.Any]
670670
:returns: Schema names for the columns that were created.
671-
:rtype: :class:`list` of :class:`str`
671+
:rtype: list[str]
672672
Example:
673673
Create multiple columns on the custom table::
674674
@@ -703,9 +703,9 @@ def delete_columns(
703703
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"``).
704704
:type table_schema_name: :class:`str`
705705
:param columns: Column name or list of column names to remove. Must include customization prefix value (e.g. ``"new_TestColumn"``).
706-
:type columns: :class:`str` or :class:`list` of :class:`str`
706+
:type columns: str or list[str]
707707
:returns: Schema names for the columns that were removed.
708-
:rtype: :class:`list` of :class:`str`
708+
:rtype: list[str]
709709
Example:
710710
Remove two custom columns by schema name:
711711

src/PowerPlatform/Dataverse/models/table_info.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ class TableInfo:
9797
:param description: Table description.
9898
:type description: :class:`str` or None
9999
:param columns: Column metadata (when retrieved).
100-
:type columns: :class:`list` of :class:`ColumnInfo` or None
100+
:type columns: list[ColumnInfo] or None
101101
:param columns_created: Column schema names created with the table.
102-
:type columns_created: :class:`list` of :class:`str` or None
102+
:type columns_created: list[str] or None
103103
104104
Example::
105105
@@ -241,7 +241,7 @@ class AlternateKeyInfo:
241241
:param schema_name: Key schema name.
242242
:type schema_name: :class:`str`
243243
:param key_attributes: List of column logical names that compose the key.
244-
:type key_attributes: :class:`list` of :class:`str`
244+
:type key_attributes: list[str]
245245
:param status: Index creation status (``"Active"``, ``"Pending"``, ``"InProgress"``, ``"Failed"``).
246246
:type status: :class:`str`
247247
"""

src/PowerPlatform/Dataverse/operations/dataframe.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ def get(
7575
:param record_id: Optional GUID to fetch a specific record. If None, queries multiple records.
7676
:type record_id: :class:`str` or None
7777
:param select: Optional list of attribute logical names to retrieve.
78-
:type select: :class:`list` of :class:`str` or None
78+
:type select: list[str] or None
7979
:param filter: Optional OData filter string. Column names must use exact lowercase logical names.
8080
:type filter: :class:`str` or None
8181
:param orderby: Optional list of attributes to sort by.
82-
:type orderby: :class:`list` of :class:`str` or None
82+
:type orderby: list[str] or None
8383
:param top: Optional maximum number of records to return.
8484
:type top: :class:`int` or None
8585
:param expand: Optional list of navigation properties to expand (case-sensitive).
86-
:type expand: :class:`list` of :class:`str` or None
86+
:type expand: list[str] or None
8787
:param page_size: Optional number of records per page for pagination.
8888
:type page_size: :class:`int` or None
8989

src/PowerPlatform/Dataverse/operations/query.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ def sql(self, sql: str) -> List[Record]:
5151
5252
:return: List of :class:`~PowerPlatform.Dataverse.models.record.Record`
5353
objects. Returns an empty list when no rows match.
54-
:rtype: :class:`list` of
55-
:class:`~PowerPlatform.Dataverse.models.record.Record`
54+
:rtype: list[~PowerPlatform.Dataverse.models.record.Record]
5655
5756
:raises ~PowerPlatform.Dataverse.core.errors.ValidationError:
5857
If ``sql`` is not a string or is empty.

0 commit comments

Comments
 (0)