fix(core): fall back to CLSID when Photoshop publishes no versioned ProgID - #435
Merged
loonghao merged 2 commits intoOct 1, 2026
Merged
Conversation
…rogID Some Photoshop installations, notably portable or relocated copies, register their automation classes under a bare CLSID and never publish the versioned `Photoshop.<Class>.<version>` ProgIDs that this library resolves classes through. Those classes are perfectly creatable, but every wrapper raised "Please check if you have Photoshop installed correctly.", so only `Application()` worked. `_get_application_object` now resolves the class ID once every ProgID lookup has failed. The ID comes from Photoshop's own type library, which declares every automation coclass by name, and falls back to asking each registered Photoshop CLSID for its name over IDispatch when no type library is available. Both routes are cached for the lifetime of the process, and a working ProgID still wins, so existing behaviour is unchanged. The failure message now lists each program ID that was attempted and the outcome of the class ID fallback instead of always pointing at a missing installation.
The previous change resolved Photoshop classes through their type library when no versioned ProgID is published, but none of that code was exercised by the test suite: every case stubbed the resolver out, so a completely broken type library index still passed all 13 tests. Since CI runs without Photoshop, the main path was the one thing CI could not catch. test/test_clsid_resolution.py runs the resolver itself against an in-memory registry and a fake type library, covering the type library index, the registry walk, the install directory lookup, the LocalServer32 command parsing, and the IDispatch probe. The suite now fails if the index returns nothing, if the richest-library sort is dropped, or if a sibling install such as "Photoshop 2024 Beta" is mistaken for "Photoshop 2024". Three failure modes found while writing those tests: - `_is_within` matched on a bare name prefix, so a sibling directory sharing a prefix counted as being inside the install directory and could feed another version's type library to LoadTypeLibEx. The comparison is now anchored on a path separator. - `winreg.OpenKey` was called bare while walking the TypeLib hive, so a key that disappeared mid-walk raised out of the resolver instead of degrading to the normal "could not resolve" error. The walk now tolerates unreadable keys, and `_core` records the real reason when the resolver itself raises. - A miss was cached for the life of the process even when no Photoshop had been seen, so a lookup that ran before Photoshop started kept failing afterwards. Misses are now only cached once Photoshop has actually been found, and the error reports whether the class is genuinely unknown or whether the registered classes simply could not be inspected. Also skip the TypeLib walk entirely when there is no install directory, since every candidate would be discarded anyway.
loonghao
force-pushed
the
feat/photoshop-python-api-photoshop-com-clsid-a0ff335153cd
branch
from
September 30, 2026 23:40
8bdb12e to
e6be49d
Compare
loonghao
deleted the
feat/photoshop-python-api-photoshop-com-clsid-a0ff335153cd
branch
October 1, 2026 03:48
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.
Problem
On some Photoshop installations the automation classes are registered under a bare CLSID and the versioned
Photoshop.<Class>.<version>ProgIDs are never published. Every wrapper class then failed with:while
photoshop.api.Application()worked fine, becausePhotoshop.Applicationis one of the few ProgIDs that are published.Concretely, on such an install only 6 ProgIDs exist (
Photoshop.Application,Photoshop.Application.190,Photoshop.Application.190.1,Photoshop.Image,Photoshop.Image.26,Photoshop.PlugIn), while 50 CLSIDs are registered asLocalServer32entries pointing atPhotoshop.exeand 48 of them are creatable.Session()and the 21 helper wrappers were all unusable.Fix
Photoshop._get_application_objectnow falls back to the class ID once every ProgID lookup has failed:IDispatch::GetTypeInfo. This depends only on Photoshop itself, but costs roughly a second per candidate, so it is strictly a last resort.Both routes are cached for the lifetime of the process. A working ProgID still wins, so nothing changes on installations that publish ProgIDs.
The failure message now reports what was actually tried instead of always blaming a missing installation:
Changes
photoshop/api/_clsid.py(new): class ID resolution from Photoshop's type library, with theIDispatchprobe as a fallback.photoshop/api/_core.py: CLSID fallback in_get_application_object, plus the diagnostic error message.test/test_clsid_fallback.py(new): unit tests for how the fallback is wired intoPhotoshop, with the resolver stubbed out.test/test_clsid_resolution.py(new): tests that run the resolver itself against an in-memory registry and a fake type library.Why a second test file
The first suite stubs the resolver, so it verifies the wiring but not the resolution. A review pointed out that this left the type library path — the one that carries the whole feature, and the one CI cannot exercise because it runs without Photoshop — with no automated coverage at all: breaking the index entirely still passed all 13 tests.
The second suite runs the real code and fails if the type library index returns nothing, if the richest-library sort is dropped, or if a sibling install such as
Photoshop 2024 Betais mistaken forPhotoshop 2024.Three failure modes surfaced while writing it:
_is_withinmatched on a bare name prefix, so a sibling directory sharing a prefix counted as being inside the install directory and could feed another version's type library toLoadTypeLibEx. The comparison is now anchored on a path separator.winreg.OpenKeywas called bare while walking theTypeLibhive, so a key that disappeared mid-walk raised out of the resolver instead of degrading to the normal "could not resolve" error. The walk now tolerates unreadable keys, and_corerecords the real reason when the resolver itself raises.Also skip the
TypeLibwalk entirely when there is no install directory, since every candidate would be discarded anyway.Verification
pytest— 51 passed.black --check,isort --check-only,flake8clean.Session(action="new_document")no longer raises; the README example runs end to end and writes a JPEG.JPEGSaveOptions(quality=5),SolidColor(),ActionDescriptor(),ActionReference()and the other 16 wrapper classes all construct.Application()still resolves through its ProgID in a singleCreateObjectcall, so the existing path is untouched.[48, 1]richest-first, and resolves all 22 coclass names the library uses with none missing._is_withinto a bare prefix match, removing the install-dir short circuit, and dropping quote handling all make the suite fail.