From 79da87f0954cdbdb3637517fdd27f739b51fd355 Mon Sep 17 00:00:00 2001 From: Yang Zhao Date: Wed, 11 Feb 2026 09:20:18 -0800 Subject: [PATCH] Add type stubs for P4 and P4API This adds fairly comprehensive type annotations for P4API, and partially complete annotations for P4. P4.P4 is difficult to annotate more precisely as the run()-based functions have very loose argument semantics, and their outputs can very wildly based on input and internal state. Note that this addition does not currently support packaging. Python does not currently define a mechanism for indicating that a shared native library located outside of a package folder has type information. --- P4.pyi | 285 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ P4API.pyi | 205 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 490 insertions(+) create mode 100644 P4.pyi create mode 100644 P4API.pyi diff --git a/P4.pyi b/P4.pyi new file mode 100644 index 0000000..5cafca2 --- /dev/null +++ b/P4.pyi @@ -0,0 +1,285 @@ +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from datetime import datetime +from pathlib import Path +from types import TracebackType +from typing import ( + Any, + ClassVar, + Final, + Literal, + NoReturn, + TypedDict, + overload, + type_check_only, +) + +import sys + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + +import P4API + +class P4Exception(Exception): + value: str + errors: list[str] | None + warnings: list[str] | None + def __init__(self, value: str | tuple[str, list[str], list[str]]) -> None: ... + +class Spec(dict[str, str | list[str]]): + def __init__(self, fieldmap: Mapping[str, str] | None = None) -> None: ... + def __getattr__(self, attr: str) -> str | list[str]: ... + def __setattr__(self, attr: str, value: str | list[str]) -> None: ... + def __setitem__(self, key: str, value: str | list[str]) -> None: ... + def permitted_fields(self) -> Mapping[str, str] | None: ... + +class Integration: + how: str + file: str + srev: int + erev: int + def __init__(self, how: str, file: str, srev: int, erev: int) -> None: ... + +class Revision: + depotFile: DepotFile + integrations: list[Integration] + rev: int | None + change: int | None + action: Literal["add", "edit", "delete", "branch", "integrate"] | None + type: str | None + time: datetime | None + user: str | None + client: str | None + desc: str | None + digest: str | None + fileSize: str | None + def __init__(self, depotFile: DepotFile) -> None: ... + def integration(self, how: str, file: str, srev: int, erev: int) -> Integration: ... + def each_integration(self) -> Iterator[Integration]: ... + +class DepotFile: + depotFile: str + revisions: list[Revision] + def __init__(self, name: str) -> None: ... + def new_revision(self) -> Revision: ... + def each_revision(self) -> Iterator[Revision]: ... + def str_revision(self, rev: Revision, revFormat: str, changeFormat: str) -> str: ... + def str_integration(self, integ: Integration) -> str: ... + +class Resolver: + def resolve(self, mergeInfo: P4API.P4MergeData, /) -> P4API.ResolveAction: ... + def actionResolve( + self, mergeInfo: P4API.P4ActionMergeData, / + ) -> P4API.ResolveAction: ... + +class OutputHandler: + REPORT: ClassVar = 0 + HANDLED: ClassVar = 1 + CANCEL: ClassVar = 2 + + def outputText(self, s: str, /) -> _HandlerResult: ... + def outputBinary(self, b: bytes, /) -> _HandlerResult: ... + def outputStat(self, h: Spec | dict[str, Any], /) -> _HandlerResult: ... + def outputInfo(self, i: str, /) -> _HandlerResult: ... + def outputMessage(self, e: P4API.P4Message, /) -> _HandlerResult: ... + +_HandlerResult: TypeAlias = Literal[0, 1, 2] + +class ReportHandler(OutputHandler): ... + +class FilelogOutputHandler(OutputHandler): + def outputFilelog(self, f: DepotFile) -> _HandlerResult: ... + +class Progress: + TYPE_SENDFILE: ClassVar = 1 + TYPE_RECEIVEFILE: ClassVar = 2 + TYPE_TRANSFER: ClassVar = 3 + TYPE_COMPUTATION: ClassVar = 4 + + UNIT_PERCENT: ClassVar = 1 + UNIT_FILES: ClassVar = 2 + UNIT_KBYTES: ClassVar = 3 + UNIT_MBYTES: ClassVar = 4 + + def init(self, type: Literal[1, 2, 3, 4, 5, 6, 7]) -> None: ... + def setDescription( + self, description: str, units: Literal[0, 1, 2, 3, 4, 5, 6, 7] + ) -> None: ... + def setTotal(self, total: int) -> None: ... + def update(self, position: int) -> None: ... + def done(self, fail: bool | int) -> None: ... + +class TextProgress(Progress): + TYPES: Final[list[str]] + UNITS: Final[list[str]] + + def init(self, type: Literal[1, 2, 3, 4, 5, 6, 7]) -> None: ... + def setDescription( + self, description: str, units: Literal[0, 1, 2, 3, 4, 5, 6, 7] + ) -> None: ... + def setTotal(self, total: int) -> None: ... + def update(self, position: int) -> None: ... + def done(self, fail: bool | int) -> None: ... + +def processFilelog(h: Mapping[str, Any]) -> DepotFile: ... + +class Map(P4API.P4Map): + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, maps: list[str]) -> None: ... + @overload + def __init__(self, map: str) -> None: ... + @overload + def __init__(self, lhr: str, rhs: str) -> None: ... + + LEFT2RIGHT: ClassVar = True + RIGHT2LEFT: ClassVar = False + + def is_empty(self) -> bool: ... + def includes(self, path: str, left_to_right: bool = True, /) -> bool: ... + def reverse(self) -> Map: ... + @overload + def insert(self, maps: list[str], /) -> None: ... + @overload + def insert(self, lhr: str, rhs: str | None = None, /) -> None: ... + +class P4(P4API.P4Adapter): + # See https://help.perforce.com/helix-core/apis/p4python/current/Content/P4Python/python.p4.html + # for documented public API. Most documented attributes are inherited from P4API, and are annotated there instead. + + RAISE_ALL: ClassVar = 2 + RAISE_ERROR: ClassVar = 1 + RAISE_ERRORS: ClassVar = 1 + RAISE_NONE: ClassVar = 0 + + EV_USAGE: ClassVar = 0x01 + EV_UNKNOWN: ClassVar = 0x02 + EV_CONTEXT: ClassVar = 0x03 + EV_ILLEGAL: ClassVar = 0x04 + EV_NOTYET: ClassVar = 0x05 + EV_PROTECT: ClassVar = 0x06 + + EV_EMPTY: ClassVar = 0x11 + + EV_FAULT: ClassVar = 0x21 + EV_CLIENT: ClassVar = 0x22 + EV_ADMIN: ClassVar = 0x23 + EV_CONFIG: ClassVar = 0x24 + EV_UPGRADE: ClassVar = 0x25 + EV_COMM: ClassVar = 0x26 + EV_TOOBIG: ClassVar = 0x27 + + E_EMPTY: ClassVar = 0 + E_INFO: ClassVar = 1 + E_WARN: ClassVar = 2 + E_FAILED: ClassVar = 3 + E_FATAL: ClassVar = 4 + + specfields: Final[dict[str, tuple[str, str]]] + + @classmethod + def identify(cls) -> str: ... + def __getattr__(self, name: str) -> Any: ... + def __init__(self, **kwargs: Any) -> None: ... + def is_ignored(self, path: str | Path) -> bool: ... + def log_messages(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> Literal[False]: ... + @contextmanager + def while_tagged(self, t: bool) -> Iterator[None]: ... + @contextmanager + def at_exception_level(self, e: Literal[0, 1, 2]) -> Iterator[None]: ... + @contextmanager + def using_handler(self, c: OutputHandler) -> Iterator[None]: ... + @contextmanager + def saved_context(self, **kargs: Any) -> Iterator[None]: ... + @contextmanager + def temp_client(self, prefix: str, template: str) -> Iterator[str]: ... + def run(self, /, *args: Any, **kargs: Any) -> _P4Result: ... + + # Command methods that have explicit implementations with additional processing + def run_submit( + self, + *args: Any | Spec | dict[str, str | list[str]], + progress: Progress | None = None, + **kargs: Any, + ) -> _P4Result: ... + def run_shelve( + self, + *args: Any | Spec | dict[str, str | list[str]], + **kargs: Any, + ) -> _P4Result: ... + @overload + def delete_shelve( + self, + changelist_number: int | str, + /, + *args: str, + **kargs, + ) -> list[str]: ... + @overload + def delete_shelve(self, *args: Any, **kargs: Any) -> list[str]: ... + def run_login(self, *args: Any, **kargs: Any) -> _P4Result: ... + def run_password( + self, oldpass: str, newpass: str, *args: Any, **kargs: Any + ) -> list[str]: ... + def run_filelog(self, *args: Any, **kwargs: Any) -> list[DepotFile | str]: ... + def run_print(self, *args: Any, **kargs) -> list[str | bytes]: ... + def run_resolve( + self, + *args: Any, + resolver: Resolver | None = None, + **kargs: Any, + ) -> _P4Result: ... + def run_tickets(self) -> list[TicketInfo]: ... + def run_init(self, *args: Any, **kargs: Any) -> NoReturn: ... + def run_clone(self, *args: Any, **kargs: Any) -> NoReturn: ... + +_P4Result: TypeAlias = list[dict[str, Any] | Spec | str] + +@type_check_only +class TicketInfo(TypedDict): + Host: str + User: str + Ticket: str + +class PyKeepAlive: + def __init__(self) -> None: ... + def isAlive(self) -> Literal[0, 1] | bool: ... + +def init( + user: str | None = None, + client: str | None = None, + directory: str | None = None, + port: str | None = None, + casesensitive: bool | None = None, + unicode: bool | None = None, + **kwargs: Any, +) -> P4: ... +def clone( + user: str | None = None, + client: str | None = None, + directory: str | None = None, + depth: int | None = None, + verbose: bool | None = None, + port: str | None = None, + remote: str | None = None, + file: str | None = None, + noarchive: bool | None = None, + progress: Progress | None = None, + **kwargs: Any, +) -> P4: ... diff --git a/P4API.pyi b/P4API.pyi new file mode 100644 index 0000000..5d16ca0 --- /dev/null +++ b/P4API.pyi @@ -0,0 +1,205 @@ +from logging import Logger +from typing import Any, Literal, TypedDict + +import sys + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + +import P4 + +def identify() -> str: ... +def dvcs_init( + user: str | None = None, + client: str | None = None, + directory: str | None = None, + port: str | None = None, + casesensitive: bool | None = None, + unicode: bool | None = None, +) -> P4.P4: ... +def dvcs_clone( + user: str | None = None, + client: str | None = None, + directory: str | None = None, + depth: int | None = None, + verbose: bool | None = None, + port: str | None = None, + remote: str | None = None, + file: str | None = None, + noarchive: bool | None = None, + progress: P4.Progress | None = None, +) -> P4.P4: ... + +class P4Adapter: + def connect(self) -> Self: ... + def connected(self) -> bool: ... + def disconnect(self) -> None: ... + def env(self, var: str, /) -> str | None: ... + def set_env(self, var: str, val: str | None = None, /) -> bool: ... + def run(self, cmd: str, /, *args: Any) -> Any: ... + def format_spec(self, type_: str, dict_: dict[str, Any], /) -> str: ... + def parse_spec( + self, type_: str, form: str, / + ) -> P4.Spec | dict[str, Any] | Literal[False]: ... + def define_spec(self, type_: str, spec: str, /) -> None: ... + def protocol(self, var: str, val: str | None = None, /) -> None: ... + def disable_tmp_cleanup(self) -> None: ... + def is_ignored(self, path: str, /) -> bool: ... + def set_tunable(self, tunable: str, value: str, /) -> int: ... + def get_tunable(self, tunable: str, /) -> int: ... + def setbreak(self, keepalive: P4.PyKeepAlive, /) -> None: ... + + # From PythonClientAPI::intattributes + @property + def tagged(self) -> int: ... + @tagged.setter + def tagged(self, value: bool | int) -> None: ... + api_level: int + maxresults: int + maxscanrows: int + maxlocktime: int + maxopenfiles: int + maxmemory: int + exception_level: Literal[0, 1, 2] + @property + def debug(self) -> int: ... + @debug.setter + def debug(self, value: bool | int) -> None: ... + @property + def track(self) -> int: ... + @track.setter + def track(self, value: bool | int) -> None: ... + @property + def streams(self) -> int: ... + @streams.setter + def streams(self, value: bool | int) -> None: ... + @property + def graph(self) -> int: ... + @graph.setter + def graph(self, value: bool | int) -> None: ... + @property + def case_folding(self) -> int: ... + @case_folding.setter + def case_folding(self, value: bool | int) -> None: ... + + # From PythonClientAPI::strattributes + charset: str + client: str + @property + def p4config_file(self) -> str | None: ... + p4enviro_file: str + cwd: str + host: str + ignore_file: str + language: str + port: str + prog: str + ticket_file: str + password: str + user: str + version: str + @property + def PATCHLEVEL(self) -> str: ... + @property + def OS(self) -> str: ... + encoding: str + + # From PythonClientAPI::objattributes + input: object | None + resolver: P4.Resolver | None + handler: P4.OutputHandler | None + progress: P4.Progress | None + @property + def errors(self) -> list[str]: ... + @property + def warnings(self) -> list[str]: ... + @property + def messages(self) -> list[str | P4Message]: ... + @property + def p4config_files(self) -> list[str]: ... + @property + def track_output(self) -> list[str]: ... + @property + def server_level(self) -> int: ... + @property + def server_case_insensitive(self) -> bool: ... + @property + def server_unicode(self) -> bool: ... + logger: Logger | None + +class P4Map: + def insert(self, lhs: str, rhs: str | None = None, /) -> None: ... + def clear(self) -> None: ... + def translate( + self, path: str, left_to_right: bool | Literal[0, 1] = True, / + ) -> str | None: ... + def translate_array( + self, path: str, left_to_right: bool | Literal[0, 1] = True, / + ) -> list[str] | None: ... + def count(self) -> int: ... + def reverse(self) -> P4Map: ... + def lhs(self) -> list[str]: ... + def rhs(self) -> list[str]: ... + def as_array(self) -> list[str]: ... + @classmethod + def join(cls, left: P4Map, right: P4Map) -> P4Map: ... + +class P4ActionMergeData: + @property + def merge_action(self) -> str: ... + @property + def yours_action(self) -> str: ... + @property + def their_action(self) -> str: ... + @property + def type(self) -> str: ... + @property + def merge_hint(self) -> ResolveAction: ... + @property + def info(self) -> P4MergeDataInfo: ... + +class P4MergeDataInfo(TypedDict): + clientFile: str + fromFile: str + startFromRev: str + endFromRev: str + resolveFlag: str + resolveType: str + +class P4MergeData: + @property + def your_name(self) -> str: ... + @property + def their_name(self) -> str: ... + @property + def base_name(self) -> str: ... + @property + def your_path(self) -> str: ... + @property + def their_path(self) -> str | None: ... + @property + def base_path(self) -> str | None: ... + @property + def result_path(self) -> str | None: ... + @property + def merge_hint(self) -> ResolveAction: ... + def run_merge(self) -> bool: ... + +ResolveAction: TypeAlias = Literal["ay", "at", "am", "ae", "s", "q"] + +class P4Message: + @property + def severity(self) -> Literal[0, 1, 2, 3, 4]: ... + @property + def generic(self) -> int: ... + @property + def msgid(self) -> int: ... + @property + def dict(self) -> dict[str, str]: ...