fix(provider-generator): emit provider functions into a "functions" submodule - #400
Conversation
…ubmodule
Generated Python bindings for any provider that declares provider-defined
functions (Terraform >= 1.8) were unimportable:
File ".../imports/kubernetes/provider/__init__.py", line 40, in <module>
from .functions import (
ModuleNotFoundError: No module named 'imports.kubernetes.provider.functions'
Root cause is in jsii-pacmak's Python target. Cross-submodule type
references are rendered as relative imports computed by
`lib/targets/python/type-name.ts#relativeImportPath`, which decides "is the
target a child of me?" with a bare prefix test and no `.`-boundary check:
if (toPkg.startsWith(fromPkg)) return `.${toPkg.substring(fromPkg.length + 1)}`;
The provider class lives in the `provider` submodule and imports the
functions wrapper from a sibling submodule. With the folder named
`provider-functions`, that sibling's Python name is
`<provider>.provider_functions`, which string-prefixes
`<provider>.provider` - so pacmak treated it as a child and emitted
`from .functions import ...`, naming a module that is never written.
Renaming the emitted folder to `functions` keeps the two submodule names
prefix-disjoint, so pacmak emits the correct `from ..functions import ...`.
Only Python was affected: Go, Java and C# reference the sibling package by
its fully qualified name and never compute a relative path.
The regression test replays pacmak's own `relativeImportPath` over the
emitted layout and asserts the import it would write resolves to the
emitted functions submodule, rather than asserting the folder name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fc5f812 to
c923a26
Compare
| export * as ephemeralCachedSecret from './ephemeral-cached-secret/index'; | ||
| export * as provider from './provider/index'; | ||
| export * as providerFunctions from './provider-functions/index'; | ||
| export * as functions from './functions/index'; |
There was a problem hiding this comment.
small change, big impact.
Too bad my out-of-repo harnesses didn't really validate Python consumption of provider functions - the real issue is correctly being addressed now with raising the version ceiling of what we test of course
but if I build demo harnesses I should include JSII cross compiled library testing
https://github.com/sakul-learning/cdktn-provider-features-demo
This comment has been minimized.
This comment has been minimized.
Status after #395 / #398: no longer a blocker — the rename is now the risk#395 (jsii-pacmak 1.140.0) merged, #398 was rebased on it and went fully green, including What remains in this PR is the My own position is that the supported surface is Worth keeping, re-scoped
Suggest either re-scoping this PR to (1) + (2) and dropping the rename, or closing it and opening those two as small follow-ups. Happy with either. |
|
Agreed on all of it — closing in favour of the re-scoped follow-ups. You're right that the rename is the risk now rather than the fix. #395 landing makes it unnecessary, and "internal emit detail" was wrong of me: the jsii submodule name is a public FQN in every target, and renaming it as a non-major on packages whose versions track the upstream provider — with Split as suggested:
On the pacmak-replay test: agreed it would pass vacuously now, so it's dropped rather than carried over — #404 is the durable replacement. And you're right that the upstream |
Related issue
No filed issue — found by CI on #398, which raises the Terraform test ceiling to 1.16.1. #398 is deliberately on hold until this lands.
Description
Generated Python bindings for any provider declaring provider-defined functions are unimportable:
Root cause: a jsii-pacmak bug, triggered by our emitted layout
jsii-pacmak/lib/targets/python/type-name.ts#relativeImportPathdecides "is the target submodule a child of me?" with a bare string-prefix test and no.-boundary check:The provider class lives in the
providerjsii submodule and imports the functions wrapper from a sibling submodule. Namedprovider-functions, that sibling's Python name is<provider>.provider_functions— which string-prefixes<provider>.provider. pacmak therefore treats the sibling as a child and emitsfrom .functions import …, naming<provider>.provider.functions, a module that is never written.Renaming the emitted folder to
functionskeeps the two submodule names prefix-disjoint, so pacmak emits the correctfrom ..functions import ….Only Python is affected. Go (
.../edge/providerfunctions), Java (imports.edgeprovider.provider_functions) and C# (Providers/Edge/ProviderFunctions) reference the sibling by fully qualified name and never compute a relative path — verified in the generated edge-provider bindings.Already fixed upstream — in a pacmak we don't use yet
jsii-pacmak stopped emitting relative cross-submodule imports in 1.136.0, switching to absolute
LazyImportreferences.relativeImportPathstill exists in 1.140.0 but is dead code. Verified by inspecting each published tarball:relativeImportPathcall sitesLazyImportrefsThis is why prebuilt providers were never affected, despite being generated against a modern Terraform that emits
functions.cdktn-provider-time@14.0.1on PyPI ships both aprovider/and aprovider_functions/submodule — the exact prefix-colliding pair — yet itsprovider/__init__.pycontainsimport cdktn_provider_time.provider_functions as _provider_functions_1fc2c0c2, an absolute import, with no broken relative one. It was built with jsii 5.9.51 and a post-1.136 pacmak. Onlycdktn getrun from this repo's pinned 1.128.0 produces the broken output.So there are two ways to fix this: rename the folder (this PR), or upgrade jsii-pacmak past 1.136.0 — which #373 already proposes (
1.128.0→1.139.0+) as part of the jsii 6.0 migration.I went with the rename because it is small, independent, and unblocks #398 now, whereas #373 is a coordinated jsii/TypeScript/constructs upgrade. The rename also stays worthwhile after #373: the two submodule names become prefix-disjoint regardless of which pacmak is in use, so the layout stops depending on an upstream implementation detail. If you would rather wait for #373 and drop this, that is a reasonable call — the regression test would need rethinking, since it replays
relativeImportPathdeliberately.Why this layer
The real defect is upstream. Fixing it there means patching a bundled dependency that also ships inside
cdktn-cli. The generator owns the emitted layout and is the only thing that has to change: two emit sites plus a documented constant,PROVIDER_FUNCTIONS_FOLDER_NAME, carrying the "must not start withprovider" rationale at the point where someone would otherwise rename it back.The change is invisible to users — the functions are reached through the
provider.functionsgetter, and nothing inexamples/,test/ordocs/referenced the old submodule name.Worth reporting upstream to
aws/jsiiseparately; the fix there is a.-boundary check inrelativeImportPath.Why it was never caught
Terraform only emits a
functionssection interraform providers schema -jsonfrom 1.8 onward, and CI's ceiling was 1.6.5, so no CI job could produce a schema that reaches this codepath. That is exactly the gap #337 was filed about.test/python/edge/test.tsis alsodescribe.skip'd, which is why #311's cross-language compile coverage did not catch it either.Testing
New test in
packages/@cdktn/provider-generator/src/get/__tests__/generator/provider-functions.test.ts. Rather than asserting the folder name — a change-detector that would be re-broken by the same rename — it copies pacmak'srelativeImportPathverbatim, reads the provider→functions submodule pair back out of the generatedindex.ts, computes the import specifier pacmak would write, and asserts it resolves to the emitted functions submodule.Confirmed to be a real regression guard: with
PROVIDER_FUNCTIONS_FOLDER_NAMEreverted to"provider-functions", the suite fails 8 of 15; with the fix it passes.Also verified:
@cdktn/provider-generator: 22 suites / 107 tests / 101 snapshots pass (re-run after rebasing onto fix(provider-generator): satisfy module provider configuration aliases on get #383). 7 snapshots updated, all theproviderFunctions→functionsexport line.python/edge/functions/exists andprovider/__init__.pyhasfrom ..functions import …. An AST walk resolving every relative import in the generated Python: 22 checked, 0 unresolvable; the same walk pre-fix flags exactly one.cdktn getagainst the realhashicorp/time@0.14.1provider (which declares provider functions) withtargetLanguage: PYTHON— real schema → jsii → pacmak → Python. Produces aimports/time/functions/sibling package andfrom ..functions import TimeProviderFunctions; all 8 relative imports resolve.Not verified:
@examples/python-documentationitself againstkubernetes@~> 2.0, which needs a fullpnpm package+ example synth. Thetimerun exercises the identical path with a real functions-bearing schema, and the pre-fix symptom matched the CI traceback's file and line exactly. #398's CI is the definitive check — that example is what fails there today.Follow-ups, deliberately not here
test/python/edge/test.tsisdescribe.skip'd; un-skipping it is how this class of bug gets caught in future.<provider>_functions, which would collide on thefunctionsfolder.instance/instance_state); they don't cross-reference today, so the pacmak bug stays latent, but it is one reference away from biting again.Checklist
🤖 Generated with Claude Code