From 8fc720519be0efbbafbb3096f6c6a8971f57ee60 Mon Sep 17 00:00:00 2001 From: Dawid Dzhafarov Date: Fri, 20 Feb 2026 10:27:40 +0100 Subject: [PATCH] Support x-linode-cli-skip at attribute level --- linodecli/baked/request.py | 4 + linodecli/baked/response.py | 4 + tests/fixtures/skip_attribute_test.yaml | 138 ++++++++++++++++++++++++ tests/unit/conftest.py | 36 +++++++ tests/unit/test_request.py | 17 +++ tests/unit/test_response.py | 43 ++++++++ 6 files changed, 242 insertions(+) create mode 100644 tests/fixtures/skip_attribute_test.yaml diff --git a/linodecli/baked/request.py b/linodecli/baked/request.py index 2d31e5e07..52b5be73d 100644 --- a/linodecli/baked/request.py +++ b/linodecli/baked/request.py @@ -160,6 +160,10 @@ def _parse_request_model( for k, v in properties.items(): k = escape_arg_segment(k) + # Skip attributes with x-linode-cli-skip extension + if v.extensions.get("linode-cli-skip"): + continue + # Handle nested objects which aren't read-only and have properties if ( v.type == "object" diff --git a/linodecli/baked/response.py b/linodecli/baked/response.py index 9e64d0be2..b1d3b5f1c 100644 --- a/linodecli/baked/response.py +++ b/linodecli/baked/response.py @@ -193,6 +193,10 @@ def _parse_response_model(schema, prefix=None, nested_list_depth=0): for k, v in properties.items(): pref = prefix + "." + k if prefix else k + # Skip attributes with x-linode-cli-skip extension + if v.extensions.get("linode-cli-skip"): + continue + if ( v.type == "object" and v.properties is None diff --git a/tests/fixtures/skip_attribute_test.yaml b/tests/fixtures/skip_attribute_test.yaml new file mode 100644 index 000000000..4fb3f6ed0 --- /dev/null +++ b/tests/fixtures/skip_attribute_test.yaml @@ -0,0 +1,138 @@ +openapi: 3.0.1 +info: + title: API Specification for x-linode-cli-skip Tests + version: 1.0.0 +servers: + - url: http://localhost/v4 + +paths: + /skip/test: + x-linode-cli-command: skip-test + get: + summary: Test GET with skipped response attributes + operationId: getSkipTest + x-linode-cli-action: list + description: List items with some attributes skipped + responses: + '200': + description: Successful response with skipped attributes + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/SkipTestResponse' + page: + $ref: '#/components/schemas/PaginationEnvelope/properties/page' + pages: + $ref: '#/components/schemas/PaginationEnvelope/properties/pages' + results: + $ref: '#/components/schemas/PaginationEnvelope/properties/results' + post: + summary: Create with skipped request attributes + operationId: createSkipTest + x-linode-cli-action: create + description: Create an item with some request attributes skipped + requestBody: + description: Parameters for creating the item + required: true + content: + application/json: + schema: + required: + - visible_field + allOf: + - $ref: '#/components/schemas/SkipTestRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SkipTestResponse' + +components: + schemas: + PaginationEnvelope: + type: object + properties: + pages: + type: integer + readOnly: true + description: The total number of pages. + example: 1 + page: + type: integer + readOnly: true + description: The current page. + example: 1 + results: + type: integer + readOnly: true + description: The total number of results. + example: 1 + + SkipTestRequest: + type: object + description: Request object with skipped fields + properties: + visible_field: + type: string + description: This field should be visible + skipped_request_field: + type: string + x-linode-cli-skip: true + description: This field should be skipped in request + another_visible_field: + type: integer + description: Another visible field + skipped_both_field: + type: string + x-linode-cli-skip: true + description: This field should be skipped in both request and response + nested_object: + type: object + properties: + nested_visible_field: + type: string + description: This nested field should be visible + nested_skipped_field: + type: string + x-linode-cli-skip: true + description: This nested field should be skipped + + SkipTestResponse: + type: object + description: Response object with skipped fields + properties: + id: + type: integer + readOnly: true + description: The unique ID + visible_field: + type: string + description: This field should be visible + skipped_response_field: + type: string + x-linode-cli-skip: true + description: This field should be skipped in response + another_visible_field: + type: integer + description: Another visible field + skipped_both_field: + type: string + x-linode-cli-skip: true + description: This field should be skipped in both request and response + nested_object: + type: object + properties: + nested_visible_field: + type: string + description: This nested field should be visible + nested_skipped_field: + type: string + x-linode-cli-skip: true + description: This nested field should be skipped diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index adc5007ad..098a6079a 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -381,6 +381,42 @@ def get_openapi_for_docs_url_tests() -> OpenAPI: return _get_parsed_spec("docs_url_test.yaml") +@pytest.fixture +def get_skip_test_operation(): + """ + Creates a CLI operation for testing x-linode-cli-skip on response attributes. + + GET http://localhost/v4/skip/test + """ + spec = _get_parsed_spec("skip_attribute_test.yaml") + path = list(spec.paths.values())[0] + + return make_test_operation( + path.extensions.get("linode-cli-command", "default"), + getattr(path, "get"), + "get", + path.parameters, + ) + + +@pytest.fixture +def post_skip_test_operation(): + """ + Creates a CLI operation for testing x-linode-cli-skip on request attributes. + + POST http://localhost/v4/skip/test + """ + spec = _get_parsed_spec("skip_attribute_test.yaml") + path = list(spec.paths.values())[0] + + return make_test_operation( + path.extensions.get("linode-cli-command", "default"), + getattr(path, "post"), + "post", + path.parameters, + ) + + @pytest.fixture def mocked_config(): """ diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index b1b4b1926..81f55d07a 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -21,3 +21,20 @@ def test_handle_one_ofs(self, post_operation_with_one_ofs): assert arg_map[k].datatype == v[0] assert arg_map[k].description == v[1] assert arg_map[k].required == v[2] + + def test_skip_request_attributes(self, post_skip_test_operation): + """ + Test that request attributes with x-linode-cli-skip extension are excluded. + """ + args = post_skip_test_operation.args + arg_map = {arg.path: arg for arg in args} + + # These fields should be present + assert "visible_field" in arg_map + assert "another_visible_field" in arg_map + assert "nested_object.nested_visible_field" in arg_map + + # These fields should be skipped + assert "skipped_request_field" not in arg_map + assert "skipped_both_field" not in arg_map + assert "nested_object.nested_skipped_field" not in arg_map diff --git a/tests/unit/test_response.py b/tests/unit/test_response.py index b032e00b2..91cafca24 100644 --- a/tests/unit/test_response.py +++ b/tests/unit/test_response.py @@ -122,3 +122,46 @@ def test_array_of_objects_property(self, list_operation_for_response_test): {"subkey1": "item1", "subkey2": True}, {"subkey1": "item2", "subkey2": False}, ] + + def test_skip_response_attributes(self, get_skip_test_operation): + """ + Test that response attributes with x-linode-cli-skip extension are excluded. + """ + model = get_skip_test_operation.response_model + attr_map = {attr.name: attr for attr in model.attrs} + + # These fields should be present + assert "id" in attr_map + assert "visible_field" in attr_map + assert "another_visible_field" in attr_map + assert "nested_object.nested_visible_field" in attr_map + + # These fields should be skipped + assert "skipped_response_field" not in attr_map + assert "skipped_both_field" not in attr_map + assert "nested_object.nested_skipped_field" not in attr_map + + def test_skip_attributes_in_both_request_and_response( + self, post_skip_test_operation, get_skip_test_operation + ): + """ + Test that attributes marked with x-linode-cli-skip are excluded from both + request and response models. + """ + # Test request model + request_args = post_skip_test_operation.args + request_arg_map = {arg.path: arg for arg in request_args} + + # Test response model + response_model = get_skip_test_operation.response_model + response_attr_map = {attr.name: attr for attr in response_model.attrs} + + # The skipped_both_field should not appear in either model + assert "skipped_both_field" not in request_arg_map + assert "skipped_both_field" not in response_attr_map + + # But visible fields should appear in both + assert "visible_field" in request_arg_map + assert "visible_field" in response_attr_map + assert "another_visible_field" in request_arg_map + assert "another_visible_field" in response_attr_map