diff --git a/.chronus/changes/python-docstring-guideline-checks-2026-7-14-11-30-0.md b/.chronus/changes/python-docstring-guideline-checks-2026-7-14-11-30-0.md new file mode 100644 index 00000000000..a2ca29b89fc --- /dev/null +++ b/.chronus/changes/python-docstring-guideline-checks-2026-7-14-11-30-0.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +[Python] Generate model/client/config docstrings and targeted pylint suppressions that satisfy the updated `azure-pylint-guidelines-checker` docstring checks (`docstring-keyword-should-match-keyword-only`, `docstring-missing-param`) diff --git a/.chronus/changes/vnext-bump-2026-6-7-22-13-32.md b/.chronus/changes/vnext-bump-2026-6-7-22-13-32.md new file mode 100644 index 00000000000..3abfe412b05 --- /dev/null +++ b/.chronus/changes/vnext-bump-2026-6-7-22-13-32.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: dependencies +packages: + - "@typespec/http-client-python" +--- + +[Python] Bump tool version targets: pylint 4.0.6, mypy 2.1.0, pyright 1.1.411, azure-pylint-guidelines-checker 0.5.8 diff --git a/packages/http-client-python/eng/scripts/ci/config/pylintrc b/packages/http-client-python/eng/scripts/ci/config/pylintrc index 387021335fc..b6d3d4604cc 100644 --- a/packages/http-client-python/eng/scripts/ci/config/pylintrc +++ b/packages/http-client-python/eng/scripts/ci/config/pylintrc @@ -16,8 +16,12 @@ enable=useless-suppression # cyclic-import: because of https://github.com/PyCQA/pylint/issues/850 # too-many-arguments: Due to the nature of the CLI many commands have large arguments set which reflect in large arguments set in corresponding methods. # too-many-lines: Due to code generation many files end up with too many lines. +# no-cross-package-private-import: The guideline checker does not resolve relative +# imports (it ignores the import level), so legitimate same-package relative imports +# like `from ...operations._operations import ...` in generated async code are +# misreported as cross-package private imports. # Let's black deal with bad-continuation -disable=missing-docstring,locally-disabled,fixme,cyclic-import,too-many-arguments,invalid-name,duplicate-code,too-few-public-methods,consider-using-f-string,super-with-arguments,redefined-builtin,import-outside-toplevel,client-suffix-needed,unnecessary-dunder-call,unnecessary-ellipsis,disallowed-name,consider-using-max-builtin,unknown-option-value,file-needs-copyright-header,too-many-positional-arguments +disable=missing-docstring,locally-disabled,fixme,cyclic-import,too-many-arguments,invalid-name,duplicate-code,too-few-public-methods,consider-using-f-string,super-with-arguments,redefined-builtin,import-outside-toplevel,client-suffix-needed,unnecessary-dunder-call,unnecessary-ellipsis,disallowed-name,consider-using-max-builtin,unknown-option-value,file-needs-copyright-header,too-many-positional-arguments,no-cross-package-private-import [FORMAT] max-line-length=120 diff --git a/packages/http-client-python/eng/scripts/ci/dev_requirements.txt b/packages/http-client-python/eng/scripts/ci/dev_requirements.txt index adfd8faac20..a52b260b058 100644 --- a/packages/http-client-python/eng/scripts/ci/dev_requirements.txt +++ b/packages/http-client-python/eng/scripts/ci/dev_requirements.txt @@ -1,8 +1,8 @@ -pyright==1.1.407 -pylint==4.0.4 +pyright==1.1.411 +pylint==4.0.6 tox==4.16.0 tox-uv -mypy==1.19.1 +mypy==2.1.0 colorama==0.4.6 debugpy==1.8.2 pytest==9.0.3 diff --git a/packages/http-client-python/generator/pygen/codegen/models/client.py b/packages/http-client-python/generator/pygen/codegen/models/client.py index abb4d83c49d..82bdcdf1508 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/client.py +++ b/packages/http-client-python/generator/pygen/codegen/models/client.py @@ -53,6 +53,17 @@ def description(self) -> str: def name(self) -> str: return self.yaml_data["name"] + @property + def has_docstring_only_keyword(self) -> bool: + """Whether the constructor docstring documents a ``:keyword:`` that is not a real + keyword-only argument (it is popped from ``kwargs`` instead). + + This happens for params routed to ``**kwargs`` (e.g. ``api_version``): they are + still rendered as ``:keyword:`` in the docstring, so we must silence the + ``docstring-keyword-should-match-keyword-only`` guideline check. + """ + return any(p for p in self.parameters.method if p.method_location == ParameterMethodLocation.KWARG) + class Client(_ClientConfigBase[ClientGlobalParameterList]): """Model representing our service client""" @@ -187,6 +198,8 @@ def pylint_disable(self) -> str: retval = add_to_pylint_disable(retval, "too-many-instance-attributes") if len(self.name) > NAME_LENGTH_LIMIT: retval = add_to_pylint_disable(retval, "name-too-long") + if self.has_docstring_only_keyword or self.has_public_lro_operations: + retval = add_to_pylint_disable(retval, "docstring-keyword-should-match-keyword-only") return retval @property @@ -375,6 +388,8 @@ def pylint_disable(self) -> str: retval = add_to_pylint_disable("", "too-many-instance-attributes") if self.code_model.is_azure_flavor else "" if len(self.name) > NAME_LENGTH_LIMIT: retval = add_to_pylint_disable(retval, "name-too-long") + if self.has_docstring_only_keyword: + retval = add_to_pylint_disable(retval, "docstring-keyword-should-match-keyword-only") return retval @property diff --git a/packages/http-client-python/generator/pygen/codegen/models/operation_group.py b/packages/http-client-python/generator/pygen/codegen/models/operation_group.py index 5c0f467806c..798c7ae5b96 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/operation_group.py +++ b/packages/http-client-python/generator/pygen/codegen/models/operation_group.py @@ -82,6 +82,8 @@ def pylint_disable(self) -> str: retval: str = "" if self.has_abstract_operations: retval = add_to_pylint_disable(retval, "abstract-class-instantiated") + if not self.is_mixin: + retval = add_to_pylint_disable(retval, "docstring-missing-param") if len(self.operations) > 20: retval = add_to_pylint_disable(retval, "too-many-public-methods") if len(self.class_name) > NAME_LENGTH_LIMIT and self.class_name[0] != "_": diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py index 87a0b2cc4aa..15e82108a89 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py @@ -23,7 +23,7 @@ ) from .import_serializer import FileImportSerializer from .base_serializer import BaseSerializer -from ..models.utils import NamespaceType +from ..models.utils import NamespaceType, add_to_pylint_disable def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements @@ -122,6 +122,15 @@ def input_documentation_string(prop: Property) -> list[str]: # building the param line of the property doc return _documentation_string(prop, "keyword", "paramtype") + def keyword_documentation_string(self, model: ModelType) -> list[str]: + # building the ``:keyword:``/``:paramtype:`` lines for the keyword-only + # arguments of the generated ``__init__`` overload so that the docstring + # keeps a 1:1 correlation with the keyword-only arguments in the signature. + retval: list[str] = [] + for prop in sorted(self._init_line_parameters(model), key=lambda x: x.optional): + retval.extend(self.input_documentation_string(prop)) + return retval + @staticmethod def variable_documentation_string(prop: Property) -> list[str]: return _documentation_string(prop, "ivar", "vartype") @@ -199,6 +208,16 @@ def pylint_disable_items(self, model: ModelType) -> list[str]: def pylint_disable(self, model: ModelType) -> str: return " # pylint: disable=" + ", ".join(self.pylint_disable_items(model)) + def class_pylint_disable(self, model: ModelType) -> str: + """Class-level pylint disables for the model declaration line.""" + retval = model.pylint_disable() + # When the model's only constructor is ``def __init__(self, *args, **kwargs)`` (i.e. no + # typed overload is generated), the guideline checker reports the ``*args`` vararg as an + # undocumented param. There is no meaningful param to document, so silence the check. + if not self.need_init(model) and self.initialize_properties(model): + retval = add_to_pylint_disable(retval, "docstring-missing-param") + return retval + def global_pylint_disables(self) -> str: return "" @@ -244,7 +263,7 @@ def declare_model(self, model: ModelType) -> str: ) if model.parents: basename = ", ".join([m.name for m in model.parents]) - return f"class {model.name}({basename}):{model.pylint_disable()}" + return f"class {model.name}({basename}):{self.class_pylint_disable(model)}" @staticmethod def get_properties_to_initialize(model: ModelType) -> list[Property]: @@ -407,7 +426,7 @@ def declare_model(self, model: ModelType) -> str: basename = ", ".join([m.name for m in model.parents]) if model.discriminator_value: basename += f", discriminator='{model.discriminator_value}'" - return f"class {model.name}({basename}):{model.pylint_disable()}" + return f"class {model.name}({basename}):{self.class_pylint_disable(model)}" @staticmethod def get_properties_to_declare(model: ModelType) -> list[Property]: diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index e51a7645dde..40fde9fa098 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -161,7 +161,15 @@ def _is_readonly(p): class SdkJSONEncoder(JSONEncoder): - """A JSON encoder that's capable of serializing datetime objects and bytes.""" + """A JSON encoder that's capable of serializing datetime objects and bytes. + + :param args: Additional positional arguments passed to the base ``JSONEncoder``. + :type args: typing.Any + :keyword exclude_readonly: Whether to exclude readonly properties. Defaults to False. + :paramtype exclude_readonly: bool + :keyword format: The format to use for serialization. Defaults to None. + :paramtype format: typing.Optional[str] + """ def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs): super().__init__(*args, **kwargs) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 index 8bcbbf4af41..6a2b533ae55 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_dpg.py.jinja2 @@ -16,6 +16,11 @@ {% endfor %} {% endfor %} {% endif %} + {% if serializer.need_init(model) %} + {% for line in serializer.keyword_documentation_string(model) %} + {{ macros.wrap_model_string(line, '\n ') -}} + {% endfor %} + {% endif %} """ {% if model.is_polymorphic %} diff --git a/packages/http-client-python/generator/pygen/codegen/templates/serialization.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/serialization.py.jinja2 index 65a9c1f8f9e..8194e5c1566 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/serialization.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/serialization.py.jinja2 @@ -476,7 +476,11 @@ def _decode_attribute_map_key(key): class Serializer: # pylint: disable=too-many-public-methods - """Request object model serializer.""" + """Request object model serializer. + + :param classes: Mapping of model names to model types, used to resolve models during serialization. + :type classes: typing.Optional[typing.Mapping[str, type]] + """ basic_types = {str: "str", int: "int", bool: "bool", float: "float"} diff --git a/packages/http-client-python/tests/tox.ini b/packages/http-client-python/tests/tox.ini index a94a60603e4..ecdc52886ba 100644 --- a/packages/http-client-python/tests/tox.ini +++ b/packages/http-client-python/tests/tox.ini @@ -69,7 +69,7 @@ deps = -r {tox_root}/requirements/azure.txt -e {tox_root}/../generator commands = - uv pip install azure-pylint-guidelines-checker==0.5.2 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + uv pip install azure-pylint-guidelines-checker==0.5.8 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" python {tox_root}/install_packages.py azure {tox_root} python {tox_root}/../eng/scripts/ci/run_pylint.py -t azure -s generated {posargs}