Fix #166: derive examples from allOf compositions of non-object schemas - #177
Open
dstrodtman wants to merge 2 commits into
Open
Fix #166: derive examples from allOf compositions of non-object schemas#177dstrodtman wants to merge 2 commits into
dstrodtman wants to merge 2 commits into
Conversation
'example_from_schema' merged every 'allOf' subschema's example into a dict. That holds only while every subschema is an object. A subschema of any other type yields a scalar or a list, and 'dict.update' on those raises 'ValueError: dictionary update sequence element #0 has length 1; 2 is required', aborting the build. An instance of a schema composed of non-object subschemas is a value of that type, so return the subschema's example instead of merging it. Subschemas that carry annotations only, a lone 'description' being the common case, contribute no example and are skipped, since they'd otherwise resolve to the any-type default and shadow the composition. Signed-off-by: Douglas Strodtman <douglas@anyscale.com>
Returning the first non-object example short-circuited the loop, so the outcome depended on where in 'allOf' a subschema sat: composing an explicit 'example' with a bare type yielded the bare type's default whenever the latter came first. Visit every subschema instead, and prefer an explicitly provided example over a derived one. Also skip a boolean subschema, which OAS 3.1 permits and which raised 'AttributeError' on the keyword lookup, and pin the unsatisfiable object-plus-non-object composition with a test. Signed-off-by: Douglas Strodtman <douglas@anyscale.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #166.
example_from_schemamerges eachallOfsubschema's example into a dict:That's valid only while every subschema is an object. A subschema of any other type resolves to a scalar or a list, and
dict.updateon those raisesValueError: dictionary update sequence element #0 has length 1; 2 is required, which aborts the build rather than degrading. The reporter'sallOftoward atype: stringcomponent reproduces it exactly.An instance of a schema composed with
allOffrom non-object subschemas is a value of that type, not a mapping — so the composed example is that value, and there is nothing to merge it into. The fix returns it.Every subschema is still visited rather than returning at the first non-object one, so the result does not depend on the order subschemas are written in, and an explicitly provided
examplewins over one derived from a bare type. Returning early madeallOf: [{type: string}, {type: string, example: "PENDING"}]yield"string"while the same two subschemas in the other order yielded"PENDING". A composition mixing an object with a non-object cannot be satisfied by any instance, so there is no right answer; it resolves to the non-object example and there is a test pinning that, rather than leaving it to be discovered.There's a second way into the same crash, which the fix also has to handle: a subschema carrying annotations only.
{"description": ...}has notype, so it hits the any-type branch and resolves to1, andexample.update(1)then raisesTypeError: 'int' object is not iterable. Note that is a different exception from theValueErrorin the issue title — same line, same cause, so worth knowing if you reproduce it and see a type error instead. Merging isn't the right answer either: an annotation-only subschema contributes no example, and if it were treated as a non-object subschema per the paragraph above, its1would shadow the composition's real example. So subschemas with none of the example-bearing keywords are skipped.A boolean subschema is skipped as well — OAS 3.1 permits
allOf: [true], and the keyword lookup raisedAttributeErroron it.Two
allOfsemantics now coexist, and they agree hereWorth stating outright rather than leaving you to derive it:
_httpdomain._resolve_combining_schemamergesallOfkeyword-wise to build the JSON schema description, whileexample_from_schemacomposes examples per subschema. Those are different operations on the same keyword, so it is fair to ask whether they can disagree on the same input.The renderer test added here is evidence that they agree on the case that matters. For a
statusproperty composed toward a string enum, the description path emits:resjsonobj status: string:enumand the example path emits"status": "PENDING"in the same rendered output — the marker and the example body describe the same value.Why this shows up a lot
The idiom that triggers it is what code generators emit for a documented enum property — a
$refwrapped inallOfso a siblingdescriptioncan attach, since$refsiblings are ignored in OAS 3.0:Ray's Jobs API spec hits it twice this way, from two separate generated enum properties. A sibling
allOftoward an actual object in the same schema is fine, which isolates it to the non-object target rather than toallOf. So I would expect this to affect generated specs fairly broadly rather than being a corner case.Measured on a generator-shaped spec
Beyond the unit tests, I built a spec with the shape described above — two
type: stringenums referenced throughallOfwith a siblingdescription, plus oneallOftoward a real object carrying an annotation-only sibling — and compared this branch against 0.9.0 installed from PyPI, running from a neutral working directory so that the checkout couldn't shadow the released package:and the body renders:
{"type": "SUBMISSION", "status": "PENDING", "driver_info": {"id": "string"}}Both the enum properties and the annotation-only composition come out right, which is the case that motivated the fix.
Tests
Two parametrized cases in
tests/test_schema_utils.pycoverallOftoward a string with anexample, toward a stringenum, toward an integer, toward an array, and the annotation-only subschema. One renderer test intests/renderers/httpdomain/test_render_restructuredtext_markup.pycovers the whole path with:generate-examples-from-schemas:, since the crash is only reachable through it. All three fail before the change.Full suite passes (505), and
pytest --regenerate-rendered-specs tests/produces no fixture diff — none of the committed example specs use this idiom.I also ran the issue's reproducer as a real Sphinx build: it aborts before the change and renders
{"timestamp": "2022-09-12 13:09:16:18"}after.Note on the neighbouring issues
This shares a seam with #172 and #168, which are both about how a scalar-derived example becomes a rendered body — #172 has a schema-derived string emitted unencoded into an
application/jsonbody, #168 has adatetimereachingjson.dumps. I kept this PR to the crash in the composition step and did not touch the encoding step, so it is reviewable on its own. Happy to look at unifying the encoding decision separately if you think that is the right direction.Context
Found while moving the Ray docs' Jobs API reference onto this extension. Together with #165 this is what currently keeps that reference from shipping example payloads at all.