diff --git a/CHANGES b/CHANGES index e68efa95..896425e9 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,15 @@ $ uv add libvcs --prerelease allow _Notes on the upcoming release will go here._ +### Documentation + +#### Class fields describe themselves in the API reference (#548) + +The URL, command, and sync types — parsed URLs, rules, remotes, status, and +the subprocess wrapper — now say what each field holds. They previously +reached the rendered API reference as "Alias for field number 0" or as a +bare name carrying only its type. + ### Development #### CI actions updated to current majors diff --git a/src/libvcs/_internal/subprocess.py b/src/libvcs/_internal/subprocess.py index 48fa33c9..228598d7 100644 --- a/src/libvcs/_internal/subprocess.py +++ b/src/libvcs/_internal/subprocess.py @@ -72,6 +72,91 @@ def __init__(self, output: str, *args: object) -> None: class SubprocessCommand(SkipDefaultFieldsReprMixin): """Wraps a :mod:`subprocess` request. Inspect, mutate, control before invocation. + Fields mirror the parameters of :class:`subprocess.Popen`. Each is passed + through as-is when :meth:`Popen`, :meth:`run`, :meth:`check_call`, or + :meth:`check_output` fires, and the defaults match the ones + :mod:`subprocess` uses. + + Attributes + ---------- + args : _CMD + Program and its arguments, as a sequence like ``['echo', 'hi']`` or + as a single string when ``shell`` is set. + bufsize : int + Buffering policy for the pipe file objects: ``-1`` for + :data:`io.DEFAULT_BUFFER_SIZE`, ``0`` for unbuffered, ``1`` for line + buffered in text mode. + executable : StrOrBytesPath | None + Program to execute in place of ``args[0]``, or the shell to use when + ``shell`` is set. ``None`` runs ``args[0]`` itself. + stdin : _FILE + Child's standard input: a file descriptor, a file object, + :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, or ``None`` to + inherit the parent's. + stdout : _FILE + Child's standard output, taking the same values as ``stdin``. + stderr : _FILE + Child's standard error, taking the same values as ``stdin``, plus + :data:`subprocess.STDOUT` to fold it into ``stdout``. + preexec_fn : t.Callable[[], t.Any] | None + POSIX-only callable run in the child between fork and exec, or + ``None`` to run nothing. + close_fds : bool + Close inherited file descriptors above stderr in the child before + exec. + shell : bool + Run ``args`` through the system shell instead of exec'ing it + directly. + cwd : StrOrBytesPath | None + Directory to change into before running, or ``None`` to inherit the + parent's working directory. + env : _ENV | None + Environment for the child, replacing the parent's, or ``None`` to + inherit it. + creationflags : int + Windows-only process creation flags, e.g. + :data:`subprocess.CREATE_NEW_CONSOLE`. ``0`` applies none. + startupinfo : t.Any | None + Windows-only :class:`subprocess.STARTUPINFO` controlling how the + child's window appears, or ``None`` for the defaults. + restore_signals : bool + POSIX-only: reset signals Python set to ``SIG_IGN`` back to + ``SIG_DFL`` in the child before exec. + start_new_session : bool + POSIX-only: run :func:`os.setsid` in the child, detaching it from the + parent's process group and controlling terminal. + pass_fds : t.Any + POSIX-only file descriptors to keep open in the child regardless of + ``close_fds``. The empty ``()`` passes none. + umask : int + POSIX-only umask to apply in the child before exec. ``-1`` leaves the + inherited umask alone. + pipesize : int + Capacity of the pipes opened for ``stdin``, ``stdout``, and + ``stderr``. ``-1`` keeps the operating system default. + user : str | None + POSIX-only user to switch the child to, or ``None`` to stay as the + calling user. + group : str | None + POSIX-only group to switch the child to, or ``None`` to keep the + calling group. + extra_groups : list[str] | None + POSIX-only supplementary groups for the child, or ``None`` to leave + them untouched. + universal_newlines : bool | None + Alias of ``text``, kept for backwards compatibility. ``None`` leaves + the mode to the other text options. + text : t.Literal[True] | None + Open the pipe file objects in text mode. ``None`` leaves them binary + unless ``encoding``, ``errors``, or ``universal_newlines`` selects + text. + encoding : str | None + Codec for the text-mode pipe file objects. Setting it turns text mode + on; ``None`` leaves the choice to the other text options. + errors : str | None + Decoding error handler for the text-mode pipe file objects, e.g. + ``"replace"``. Setting it turns text mode on. + Examples -------- >>> cmd = SubprocessCommand("ls") diff --git a/src/libvcs/sync/base.py b/src/libvcs/sync/base.py index 6bf94786..eea11517 100644 --- a/src/libvcs/sync/base.py +++ b/src/libvcs/sync/base.py @@ -19,6 +19,16 @@ class SyncError: """An error encountered during a sync step. + Attributes + ---------- + step : str + Name of the sync step that failed, e.g. ``"fetch"`` or ``"checkout"``. + message : str + Human-readable description of what went wrong. + exception : Exception | None + Underlying exception, or ``None`` when the step reported a failure + without raising. + Examples -------- >>> error = SyncError(step="fetch", message="remote not found") @@ -39,6 +49,15 @@ class SyncError: class SyncResult: """Result of a repository synchronization. + Attributes + ---------- + ok : bool + Whether every sync step succeeded. :meth:`SyncResult.add_error` flips + this to ``False``. + errors : list[SyncError] + Errors recorded during the sync, in the order they happened. Empty + while the sync is still clean. + Examples -------- >>> result = SyncResult() @@ -92,7 +111,15 @@ def add_error( class VCSLocation(t.NamedTuple): - """Generic VCS Location (URL and optional revision).""" + """Generic VCS Location (URL and optional revision). + + Attributes + ---------- + url : str + Repository URL, with any revision suffix stripped. + rev : str | None + Revision to check out, or ``None`` when unspecified. + """ url: str rev: str | None diff --git a/src/libvcs/sync/git.py b/src/libvcs/sync/git.py index 2c291a48..71527109 100644 --- a/src/libvcs/sync/git.py +++ b/src/libvcs/sync/git.py @@ -86,7 +86,18 @@ def __str__(self) -> str: @dataclasses.dataclass class GitRemote: - """Structure containing git working copy information.""" + """Structure containing git working copy information. + + Attributes + ---------- + name : str + Remote name as git records it, e.g. ``origin``. + fetch_url : str + URL git fetches from for this remote. + push_url : str + URL git pushes to for this remote. Same as ``fetch_url`` unless a + separate push URL is configured. + """ name: str fetch_url: str @@ -99,7 +110,31 @@ class GitRemote: @dataclasses.dataclass class GitStatus: - """Git status information.""" + """Git status information. + + Fields hold the ``# branch.*`` headers of ``git status -sb + --porcelain=2`` as strings, unconverted. Each is ``None`` when the header + is absent from the output :meth:`GitStatus.from_stdout` parsed. + + Attributes + ---------- + branch_oid : str | None + Commit SHA of ``HEAD``. ``None`` on an unborn branch, where git + reports ``(initial)`` instead of a SHA. + branch_head : str | None + Checked-out branch name, or ``(detached)`` when ``HEAD`` points at a + commit rather than a branch. + branch_upstream : str | None + Upstream branch ``HEAD`` tracks, e.g. ``origin/master``. ``None`` + when the branch has no upstream configured. + branch_ab : str | None + Ahead/behind counts as git prints them, e.g. ``+0 -0``. ``None`` + without an upstream to compare against. + branch_ahead : str | None + Commit count ahead of the upstream, taken from ``branch_ab``. + branch_behind : str | None + Commit count behind the upstream, taken from ``branch_ab``. + """ branch_oid: str | None = None branch_head: str | None = None diff --git a/src/libvcs/url/base.py b/src/libvcs/url/base.py index c3214013..d1cfa07e 100644 --- a/src/libvcs/url/base.py +++ b/src/libvcs/url/base.py @@ -28,18 +28,32 @@ def is_valid(cls, url: str, is_explicit: bool | None = None) -> bool: @dataclasses.dataclass(repr=False) class Rule(SkipDefaultFieldsReprMixin): - """A Rule represents an eligible pattern mapping to URL.""" + """A Rule represents an eligible pattern mapping to URL. + + Attributes + ---------- + label : str + Computer readable name / ID. Keys the rule inside a + :class:`RuleMap` and is recorded on a matched URL's ``rule``. + description : str + Human readable description of the URL shape the rule covers. + pattern : Pattern[str] + Regex pattern. Its named groups are assigned onto the URL object's + fields of the same name. + defaults : dict[str, str] + Values to apply to fields the pattern left unset, e.g. the + ``hostname`` and ``scheme`` a bare prefix such as ``github:`` implies. + is_explicit : bool + Is the match unambiguous with other VCS systems? e.g. git+ prefix + weight : int + Weight: Higher is more likely to win + """ label: str - """Computer readable name / ID""" description: str - """Human readable description""" pattern: Pattern[str] - """Regex pattern""" defaults: dict[str, str] = dataclasses.field(default_factory=dict) - """Is the match unambiguous with other VCS systems? e.g. git+ prefix""" is_explicit: bool = False - """Weight: Higher is more likely to win""" weight: int = 0 diff --git a/src/libvcs/url/git.py b/src/libvcs/url/git.py index ac2cb038..07ed8e4d 100644 --- a/src/libvcs/url/git.py +++ b/src/libvcs/url/git.py @@ -294,6 +294,32 @@ class GitBaseURL( ): """Git repository location. Parses URLs on initialization. + Attributes + ---------- + url : str + Location as given, kept verbatim. Every other field is filled from it + by the first :class:`~libvcs.url.base.Rule` that matches. + scheme : str | None + Transport scheme, e.g. ``https`` or ``ssh``. ``None`` for scp-style + locations such as ``git@github.com:vcs-python/libvcs.git``, which + carry no scheme. + user : str | None + User in front of the hostname, e.g. ``git``. ``None`` when the URL + omits one; :meth:`to_url` falls back to ``git`` for scp-style output. + hostname : str | None + Server hosting the repository, e.g. ``github.com``. + port : int | None + Port the URL specifies, or ``None`` to use the transport's default. + path : str | None + Server-side path to the repository with the suffix split off, e.g. + ``vcs-python/libvcs``. + suffix : str | None + Trailing ``.git`` split off the path, or ``None`` when the URL has + none. :meth:`to_url` re-appends it. + rule : str | None + :attr:`~libvcs.url.base.Rule.label` of the rule that matched, or + ``None`` when no rule did and the remaining fields stayed unset. + Examples -------- >>> GitBaseURL(url='https://github.com/vcs-python/libvcs.git') @@ -454,7 +480,21 @@ class GitAWSCodeCommitURL( URLProtocol, SkipDefaultFieldsReprMixin, ): - """Supports AWS CodeCommit git URLs.""" + """Supports AWS CodeCommit git URLs. + + Parses the fields of :class:`GitBaseURL` plus the two below. + + Attributes + ---------- + region : str | None + AWS region from a ``codecommit::://`` URL, e.g. + ``us-east-1``. ``None`` for region-less GRC URLs and for the HTTPS + and SSH forms, which carry the region inside the hostname. + rev : str | None + Commit-ish (tag, branch, ref) trailing the URL as ``@rev``, or + ``None`` when the URL names no revision. :meth:`to_url` re-appends + it. + """ # AWS CodeCommit Region region: str | None = None @@ -589,7 +629,17 @@ class GitPipURL( URLProtocol, SkipDefaultFieldsReprMixin, ): - """Supports pip git URLs.""" + """Supports pip git URLs. + + Parses the fields of :class:`GitBaseURL` plus the one below. + + Attributes + ---------- + rev : str | None + Commit-ish (tag, branch, ref) from a pip-style ``@rev``, e.g. + ``v0.10.0``. ``None`` when the URL names no revision. + :meth:`to_url` re-appends it. + """ # commit-ish (rev): tag, branch, ref rev: str | None = None diff --git a/src/libvcs/url/hg.py b/src/libvcs/url/hg.py index d20fd478..3e9b9d54 100644 --- a/src/libvcs/url/hg.py +++ b/src/libvcs/url/hg.py @@ -154,6 +154,38 @@ class HgBaseURL( ): """Mercurial repository location. Parses URLs on initialization. + Attributes + ---------- + url : str + Location as given, kept verbatim. Every other field is filled from it + by the first :class:`~libvcs.url.base.Rule` that matches. + scheme : str | None + Transport scheme, e.g. ``https``, ``ssh``, or ``hg+file``. ``None`` + when the matched rule captured none. + user : str | None + User in front of the hostname, e.g. ``hg``. ``None`` when the URL + omits one; :meth:`to_url` falls back to ``hg`` for scp-style output. + hostname : str + Server hosting the repository, e.g. ``hg.mozilla.org``. Empty when + the matched rule captures no host, as with ``hg+file://`` URLs. + port : int | None + Port the URL specifies, or ``None`` to use the transport's default. + separator : str + Character sitting between the host (and port) and the path, ``/`` + unless the URL used ``:`` or ``,``. :meth:`to_url` re-emits it. + path : str + Server-side path to the repository, e.g. ``mozilla-central/``. Empty + when the URL carries no path. + suffix : str | None + Trailing ``.git``-style decoration split off the path, or ``None`` + when the URL has none. + ref : str | None + Commit-ish (tag, branch, ref, revision) for callers to set; the + bundled rules capture no ref, so parsing leaves it ``None``. + rule : str | None + :attr:`~libvcs.url.base.Rule.label` of the rule that matched, or + ``None`` when no rule did and the remaining fields stayed unset. + Examples -------- >>> HgBaseURL(url='https://hg.mozilla.org/mozilla-central/') @@ -341,7 +373,16 @@ class HgPipURL( URLProtocol, SkipDefaultFieldsReprMixin, ): - """Supports pip hg URLs.""" + """Supports pip hg URLs. + + Parses the fields of :class:`HgBaseURL` plus the one below. + + Attributes + ---------- + rev : str | None + Commit-ish (tag, branch, ref) from a pip-style ``@rev``, e.g. + ``v1.0``. ``None`` when the URL names no revision. + """ # commit-ish (rev): tag, branch, ref rev: str | None = None diff --git a/src/libvcs/url/svn.py b/src/libvcs/url/svn.py index eb28b517..886bb82e 100644 --- a/src/libvcs/url/svn.py +++ b/src/libvcs/url/svn.py @@ -150,6 +150,36 @@ class SvnBaseURL( ): """SVN repository location. Parses URLs on initialization. + Attributes + ---------- + url : str + Location as given, kept verbatim. Every other field is filled from it + by the first :class:`~libvcs.url.base.Rule` that matches. + scheme : str | None + Transport scheme, e.g. ``https``, ``svn+ssh``, or ``svn+file``. + ``None`` for scp-style locations, which carry no scheme. + user : str | None + User in front of the hostname, e.g. ``svn``. ``None`` when the URL + omits one. + hostname : str + Server hosting the repository, e.g. ``svn.debian.org``. Empty when + the matched rule captures no host, as with ``svn+file://`` URLs. + port : int | None + Port the URL specifies, or ``None`` to use the transport's default. + separator : str + Character sitting between the host (and port) and the path, ``/`` + unless the URL used ``:`` or ``,``. :meth:`to_url` re-emits it. + path : str + Server-side path to the repository, e.g. + ``svn/aliothproj/path/in/project``. Empty when the URL carries no + path. + ref : str | None + Commit-ish (tag, branch, revision) for callers to set; the bundled + rules capture no ref, so parsing leaves it ``None``. + rule : str | None + :attr:`~libvcs.url.base.Rule.label` of the rule that matched, or + ``None`` when no rule did and the remaining fields stayed unset. + Examples -------- >>> SvnBaseURL( @@ -279,7 +309,16 @@ class SvnPipURL( URLProtocol, SkipDefaultFieldsReprMixin, ): - """Supports pip svn URLs.""" + """Supports pip svn URLs. + + Parses the fields of :class:`SvnBaseURL` plus the one below. + + Attributes + ---------- + rev : str | None + Commit-ish (tag, branch, revision) from a pip-style ``@rev``, e.g. + ``2019``. ``None`` when the URL names no revision. + """ # commit-ish (rev): tag, branch, ref rev: str | None = None