Skip to content
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
1 change: 1 addition & 0 deletions scripts/breaking_changes_checker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ IGNORE_BREAKING_CHANGES = {
| ChangedParameterOrdering | `def my_function(a, b, c=None) --> def my_function(b, c=None, a=None)` | ("ChangedParameterOrdering", "module-name", "class-name", "function-name") |
| RemovedFunctionKwargs | A function was changed to no longer accept keyword arguments. `def my_func(param, **kwargs) --> def my_func(param)` | ("RemovedFunctionKwargs", "module-name", "class-name", "function-name") |
| ChangedParameterKind | `def my_function(a, b, c) --> def my_function(a, b, *, c)` | ("ChangedParameterKind", "module-name", "class-name", "function-name") |
| ChangedParameterType | `def my_function(a: str) --> def my_function(a: int)` | ("ChangedParameterType", "module-name", "class-name", "function-name") |
| ChangedFunctionKind | `async def my_function(param) -> def my_function(param)` | ("ChangedFunctionKind", "module-name", "class-name", "function-name") |

## Advanced scenarios
Expand Down
35 changes: 35 additions & 0 deletions scripts/breaking_changes_checker/breaking_changes_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class BreakingChangeType(str, Enum):
CHANGED_PARAMETER_DEFAULT_VALUE = "ChangedParameterDefaultValue"
CHANGED_PARAMETER_ORDERING = "ChangedParameterOrdering"
CHANGED_PARAMETER_KIND = "ChangedParameterKind"
CHANGED_PARAMETER_TYPE = "ChangedParameterType"
CHANGED_FUNCTION_KIND = "ChangedFunctionKind"
REMOVED_OR_RENAMED_MODULE = "RemovedOrRenamedModule"
REMOVED_FUNCTION_KWARGS = "RemovedFunctionKwargs"
Expand Down Expand Up @@ -73,6 +74,10 @@ class BreakingChangesTracker:
"Method `{}.{}` changed its parameter `{}` from `{}` to `{}`"
CHANGED_PARAMETER_KIND_OF_FUNCTION_MSG = \
"Function `{}` changed its parameter `{}` from `{}` to `{}`"
CHANGED_PARAMETER_TYPE_MSG = \
"Method `{}.{}` changed type of its parameter `{}` from `{}` to `{}`"
CHANGED_PARAMETER_TYPE_OF_FUNCTION_MSG = \
"Function `{}` changed type of its parameter `{}` from `{}` to `{}`"
CHANGED_CLASS_FUNCTION_KIND_MSG = \
"Method `{}.{}` changed from `{}` to `{}`"
CHANGED_FUNCTION_KIND_MSG = \
Expand Down Expand Up @@ -289,9 +294,15 @@ def run_parameter_level_diff_checks(
diff[diff_type], stable_default
)
elif diff_type == "param_type":
# Check if the parameter kind (positional vs keyword) has changed
self.check_parameter_type_changed(
diff["param_type"], stable_parameters_node
)
elif diff_type == "type":
Comment thread
msyyc marked this conversation as resolved.
# Check if the parameter type annotation has changed
self.check_parameter_annotation_type_changed(
diff["type"], stable_parameters_node
)

def check_kwargs_removed(self, param_type: str, param_name: str) -> None:
if param_type == "var_keyword" and param_name == "kwargs":
Expand Down Expand Up @@ -377,6 +388,30 @@ def check_parameter_type_changed(self, diff: Dict, stable_parameters_node: Dict)
)
)

def check_parameter_annotation_type_changed(self, diff: Any, stable_parameters_node: Dict) -> None:
stable_type = stable_parameters_node[self._parameter_name].get("type")
# `create_parameters` stores a missing annotation as Python None, while an
# explicit `None` annotation is stored as the string "None". Normalize the
# missing-annotation case to a distinct display so the message is unambiguous.
display_stable = "<no annotation>" if stable_type is None else stable_type
display_current = "<no annotation>" if diff is None else diff
if self._class_name:
Comment thread
msyyc marked this conversation as resolved.
self.breaking_changes.append(
(
self.CHANGED_PARAMETER_TYPE_MSG, BreakingChangeType.CHANGED_PARAMETER_TYPE,
self._module_name, self._class_name, self._function_name, self._parameter_name,
display_stable, display_current
)
)
else:
self.breaking_changes.append(
(
self.CHANGED_PARAMETER_TYPE_OF_FUNCTION_MSG, BreakingChangeType.CHANGED_PARAMETER_TYPE,
self._module_name, self._function_name, self._parameter_name,
display_stable, display_current
)
)

def check_parameter_ordering(self) -> None:
modules = self.stable.keys() & self.current.keys()

Expand Down
147 changes: 147 additions & 0 deletions scripts/breaking_changes_checker/tests/test_breaking_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,153 @@ def test_removed_overload():
assert changes == expected_msg


def test_parameter_annotation_type_changed():
stable = {
"azure.contoso": {
"class_nodes": {
"EffectiveNetworkSecurityGroup": {
"type": "Model",
"methods": {
"__init__": {
"parameters": {
"tag_map": {
"type": "Optional[str]",
"default": None,
"param_type": "keyword_only"
}
},
"is_async": False
}
}
}
},
"function_nodes": {
"my_func": {
"parameters": {
"value": {
"type": "str",
"default": None,
"param_type": "positional_or_keyword"
}
},
"is_async": False
}
}
}
}

current = {
"azure.contoso": {
"class_nodes": {
"EffectiveNetworkSecurityGroup": {
"type": "Model",
"methods": {
"__init__": {
"parameters": {
"tag_map": {
"type": "Optional[Dict[str, List[str]]]",
"default": None,
"param_type": "keyword_only"
}
},
"is_async": False
}
}
}
},
"function_nodes": {
"my_func": {
"parameters": {
"value": {
"type": "int",
"default": None,
"param_type": "positional_or_keyword"
}
},
"is_async": False
}
}
}
}

EXPECTED = [
"(ChangedParameterType): Method `EffectiveNetworkSecurityGroup.__init__` changed type of its parameter `tag_map` from `Optional[str]` to `Optional[Dict[str, List[str]]]`",
"(ChangedParameterType): Function `my_func` changed type of its parameter `value` from `str` to `int`",
]

bc = BreakingChangesTracker(stable, current, "azure-contoso")
bc.run_checks()

changes = normalize_breaking_changes_report(bc.report_changes())
expected_msg = format_breaking_changes(EXPECTED)
assert len(bc.breaking_changes) == len(EXPECTED)
assert changes == expected_msg


def test_parameter_annotation_type_missing_vs_explicit_none():
# `a` goes from no annotation (stored as Python None) to `str`.
# `b` goes from an explicit `None` annotation (stored as the string "None") to `int`.
# The reported messages must distinguish between these two cases.
stable = {
"azure.contoso": {
"class_nodes": {},
"function_nodes": {
"my_func": {
"parameters": {
"a": {
"type": None,
"default": None,
"param_type": "positional_or_keyword"
},
"b": {
"type": "None",
Comment thread
msyyc marked this conversation as resolved.
"default": None,
"param_type": "positional_or_keyword"
}
},
"is_async": False
}
}
}
}

current = {
"azure.contoso": {
"class_nodes": {},
"function_nodes": {
"my_func": {
"parameters": {
"a": {
"type": "str",
"default": None,
"param_type": "positional_or_keyword"
},
"b": {
"type": "int",
"default": None,
"param_type": "positional_or_keyword"
}
},
"is_async": False
}
}
}
}

EXPECTED = [
"(ChangedParameterType): Function `my_func` changed type of its parameter `a` from `<no annotation>` to `str`",
"(ChangedParameterType): Function `my_func` changed type of its parameter `b` from `None` to `int`",
]

bc = BreakingChangesTracker(stable, current, "azure-contoso")
bc.run_checks()

changes = normalize_breaking_changes_report(bc.report_changes())
expected_msg = format_breaking_changes(EXPECTED)
assert len(bc.breaking_changes) == len(EXPECTED)
assert changes == expected_msg


def test_report_mode_without_setup_py():
"""Verify detect_breaking_changes.py works with --source-report and --target-report
when --target points to a directory without setup.py or pyproject.toml.
Expand Down
Loading