플러그인이 목록에 보인다는 이유로 작업이 시작되면 사용자는 설정을 바꾸기도 불안해진다. 북극성의 실제 등록 서비스와 화면 진입점을 읽고, 설정의 변화만 따로 실험할 수 있는 작은 레지스트리를 만들었다. 이 예제는 프로그램을 실행하지 않으며 기록도 합성 값만 사용한다.
정의와 프로젝트 설정을 다른 자료로 둔다
정의 목록에는 실행 대상의 식별자와 이름만 있다. 프로젝트와 프로필의 조합은 등록 여부, enabled, pinned, 제거 표시를 별도로 가진다. 같은 프로그램을 두 프로젝트에서 써도 한쪽에서 끈 선택이 다른 쪽의 설정을 바꾸지 않도록 했다. 이 조합은 예제의 자료 구분일 뿐 권한을 증명하는 인증 수단은 아니다.
원래 구현에서는 프로젝트 설정을 프로필처럼 접근하는 연결부가 있다. 옛 문서의 프로필 단위 설명만 읽으면 현재 저장 범위를 오해할 수 있다. 공유 코드는 그 연결부 전체를 복사하지 않고 독립 상태의 핵심만 드러낸다. 실제 적용에는 현재 프로젝트를 신뢰할 수 있게 묶어 주는 호출부와 저장소가 필요하다.
고정은 위치를 정하고 활성화는 사용 여부를 정한다
enabled와 pinned를 하나의 상태로 합치지 않았다. 켜져 있고 고정된 항목은 toolbar와 menu에, 켜져 있으나 고정되지 않은 항목은 menu에만 나타난다. 끈 항목은 두 목록에서 숨기지만 pinned 값은 남는다. 다시 켜면 이전 고정 선택을 복원할 수 있다. 원래 화면의 정렬과 드래그 순서까지 이 예제에 구현한 것은 아니다.
register의 pinned=False는 기존 고정을 해제하는 명령이 아니다. 명시적으로 unpin을 호출해야 바뀐다. 자동 등록은 이미 꺼 둔 항목도 다시 켜지 않는다. 환경이 갱신될 때마다 사용자의 선택을 덮어쓰지 않는 것이 목적이다. 전체 설정을 조회한 사본을 수정해도 내부 상태가 바뀌지 않도록 깊은 복사도 사용했다.
열기는 준비이고 취소 요청은 결과가 아니다
open은 enabled를 확인한 뒤 prepared=True와 executed=False를 돌려준다. 사용 가능한 설정창을 여는 것과 실제 작업의 입력·모드·계정을 확인한 실행은 별개다. 공유 코드에는 실행 함수 자체가 없다. 원래 북극성의 pluginRun 경로에는 별도의 시작 검증과 runner 호출이 있으므로 등록 반환값만 보고 실행 성공으로 표현하지 않는다.
disable이나 remove에는 진행 중인 작업을 가리키는 합성 active 자료를 선택적으로 준다. 예제는 프로필·프로젝트·프로그램이 정확히 같을 때 cancel_requested를 돌려준다. 실제 취소를 호출하거나 중단 완료를 확인하지 않는다. 원래 프로그램의 특정 분기는 프로필과 프로그램으로 대조하므로, 이 예제의 더 좁은 비교를 원래 모든 실행 경로의 보장으로 읽으면 안 된다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""Independent, in-memory synthetic registry. No task execution or persistence."""
from copy import deepcopy
def identifier(value):
if not isinstance(value, str) or not value or len(value) > 80:
raise ValueError("IDENTIFIER")
if not all(c.isascii() and (c.isalnum() or c in "-_") for c in value):
raise ValueError("IDENTIFIER")
return value
class Registry:
def __init__(self, definitions):
self._definitions = {}
for definition in definitions:
if set(definition) != {"id", "name"}:
raise ValueError("DEFINITION")
key = identifier(definition["id"])
if key in self._definitions or not isinstance(definition["name"], str) or not definition["name"].strip():
raise ValueError("DEFINITION")
self._definitions[key] = deepcopy(definition)
self._projects = {}
def _scope(self, scope):
if not isinstance(scope, tuple) or len(scope) != 2:
raise ValueError("SCOPE")
return tuple(identifier(part) for part in scope)
def _definition(self, key):
identifier(key)
if key not in self._definitions:
raise ValueError("MISSING_DEFINITION")
def _state(self, scope):
return self._projects.setdefault(self._scope(scope), {"preferences": {}, "dismissed": []})
def definitions(self):
return deepcopy(list(self._definitions.values()))
def snapshot(self, scope):
return deepcopy(self._state(scope))
def register(self, scope, key, *, automatic=False, pinned=False):
self._scope(scope)
self._definition(key)
if type(automatic) is not bool or type(pinned) is not bool:
raise ValueError("BOOLEAN")
state = self._state(scope)
if automatic and key in state["dismissed"]:
return {"registered": False, "enabled": False, "pinned": False, "reused": True, "executed": False}
existing = key in state["preferences"]
pref = state["preferences"].setdefault(key, {"enabled": True, "pinned": False})
if not automatic:
if pinned:
pref["pinned"] = True
state["dismissed"] = [item for item in state["dismissed"] if item != key]
return {"registered": True, **pref, "reused": existing, "executed": False}
def action(self, scope, key, action, *, active=None):
scope = self._scope(scope)
self._definition(key)
if action not in {"enable", "disable", "pin", "unpin", "open", "remove"}:
raise ValueError("ACTION")
if active is not None:
if not isinstance(active, dict) or set(active) != {"profile", "project", "program"}:
raise ValueError("ACTIVE_SHAPE")
for value in active.values():
identifier(value)
state = self._state(scope)
if key not in state["preferences"]:
raise ValueError("NOT_REGISTERED")
pref = state["preferences"][key]
if action == "open" and not pref["enabled"]:
raise ValueError("DISABLED")
if action == "remove":
del state["preferences"][key]
if key not in state["dismissed"]:
state["dismissed"].append(key)
elif action in {"enable", "disable"}:
pref["enabled"] = action == "enable"
elif action in {"pin", "unpin"}:
pref["pinned"] = action == "pin"
# An intent only: the caller must bind and cancel the real runner.
matching = active == {"profile": scope[0], "project": scope[1], "program": key}
return {"action": action, "prepared": action == "open", "executed": False,
"cancel_requested": bool(action in {"disable", "remove"} and matching)}
def visible(self, scope):
state = self._state(scope)
menu, toolbar = [], []
for key, pref in state["preferences"].items():
if pref["enabled"]:
menu.append(key)
if pref["pinned"]:
toolbar.append(key)
return {"menu": menu, "toolbar": toolbar}
제거한 항목이 자동으로 돌아오지 않게 한다
remove는 프로젝트의 등록 자료를 지우고 dismissed에 식별자를 남긴다. 정의 목록은 그대로다. 자동 등록이 같은 식별자를 다시 발견하면 registered=False로 끝나며 항목을 살려내지 않는다. 사용자가 정리한 메뉴를 초기화 과정이 반복해서 되돌리는 문제를 작은 상태 규칙으로 막는다.
반대로 사용자가 명시적으로 register를 호출하면 제거 표시를 지우고 새 등록을 만든다. 제거 전의 고정을 별도 복원 기록으로 보관하지 않으므로 복원 기본값은 고정 해제다. 이 차이를 기록에서도 볼 수 있다. 정의 보존을 보여 주는 예제이지 원래 앱의 파일·로그인·작업 결과 전체를 백업하거나 지우는 도구는 아니다.
24개 시험과 다섯 기록으로 확인한 범위
등록의 멱등성, 고정 유지, 비활성 항목 열기 거절, 자동 복원 차단과 명시적 복원을 시험했다. 같은 프로필의 다른 프로젝트와 다른 프로필도 분리되는지 본다. 없는 정의와 잘못된 동작·입력·active 형태는 거절하고, 실패한 요청이 기존 설정을 바꾸지 않는지도 확인한다. 외부 호출을 대신하는 성공 문구는 넣지 않았다.
원본 패키지24개 시험을 실행했고 배포 ZIP을 새 폴더에 추출한 뒤 같은24개 시험을 수행한다. 다섯 기록은 이 코드의 실제 로컬 호출로 생성했다. 아래 선택기는 그 결과를 읽기만 한다. 원래 제품의 UI·실행 취소·권한 검증을 새로 시험했다는 뜻은 아니며, 북극성의 기존 시험은 설계 근거로 읽은 자료다.
실제 관리 화면에 붙일 때 남는 책임
코드와 시험, 기록 생성기와 설명을 수정하고 재사용할 수 있다. 상태는 메모리에만 있어서 프로세스를 다시 시작하면 사라진다. 내보내기 사본은 진단용이며 복구 가능한 저장 형식이나 import 기능이 아니다. 여러 호출자가 동시에 수정하는 상황과 저장 실패도 이 예제의 계약에는 없다.
서비스에 붙이려면 인증된 현재 범위, 영속 저장, 충돌 처리, 실행 중단 확인을 따로 연결해야 한다. 나는 설정 버튼을 눌렀다는 사실과 실제 업무가 일어났다는 사실을 다른 결과로 설명하고 싶었다. 각 반환값이 무엇을 바꾸었고 무엇을 아직 확인하지 않았는지 읽을 수 있는 정도의 작은 예제로 공유한다.