Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8a01c0a
cli/__init__(refactor[typing]): Narrow parser tuple type
tony Jan 3, 2026
78c45c9
cli/_formatter(refactor[typing]): Define help theme protocol
tony Jan 3, 2026
f59314b
cli/sync(refactor[typing]): Tighten exception signature
tony Jan 3, 2026
111de23
cli/_output(refactor[typing]): Add JSON payload TypedDicts
tony Jan 3, 2026
311dafc
config(refactor[typing]): Tighten raw config aliases
tony Jan 3, 2026
2ae81a8
cli/status(refactor[typing]): Add StatusResult TypedDict
tony Jan 3, 2026
13c5020
cli/add-discover(refactor[typing]): Reduce raw config Any
tony Jan 3, 2026
2c0acc6
core/refactor[typing]: Clarify generic object types
tony Jan 3, 2026
8c209c2
cli/sync(refactor[typing]): Use JsonValue for sync events
tony Jan 3, 2026
d50649a
cli/fmt(refactor[typing]): Use object/Mapping for config input
tony Jan 3, 2026
50e788d
config(refactor[typing]): Replace Any with object in helpers
tony Jan 3, 2026
5b39955
util(refactor[typing]): Make update_dict mapping-generic
tony Jan 3, 2026
3d5b488
tests/cli(refactor[typing]): Tighten config fixture types
tony Jan 3, 2026
6500898
tests(refactor[typing]): Replace Any in CLI/config fixtures
tony Jan 3, 2026
89f3f7b
core/refactor[typing]: Replace Any in config reader
tony Jan 3, 2026
f0b83d3
cli/sync(refactor[typing]): Type repo update payloads
tony Jan 3, 2026
ee82d4e
cli/fmt(refactor[typing]): Narrow repo config inputs
tony Jan 3, 2026
40740bc
scripts(refactor[typing]): Use raw dict for remotes in gitlab generator
tony Jan 3, 2026
3d37c4b
refactor(typing): Use namespace import for typing_extensions
tony Jan 3, 2026
2d36a25
cli/sync(refactor[typing]): Fix update_repo return type
tony Jan 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
@pytest.fixture(autouse=True)
def add_doctest_fixtures(
request: pytest.FixtureRequest,
doctest_namespace: dict[str, t.Any],
doctest_namespace: dict[str, object],
) -> None:
"""Harness pytest fixtures to doctests namespace."""
from _pytest.doctest import DoctestItem
Expand Down
10 changes: 4 additions & 6 deletions scripts/generate_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import requests
import yaml
from libvcs.sync.git import GitRemote

from vcspull.cli.sync import CouldNotGuessVCSFromURL, guess_vcs

Expand Down Expand Up @@ -108,11 +107,10 @@
"path": path / reponame,
"url": f"git+ssh://{url_to_repo}",
"remotes": {
"origin": GitRemote(
name="origin",
fetch_url=f"ssh://{url_to_repo}",
push_url=f"ssh://{url_to_repo}",
),
"origin": {
"fetch_url": f"ssh://{url_to_repo}",
"push_url": f"ssh://{url_to_repo}",
},
},
"vcs": vcs,
}
Expand Down
44 changes: 22 additions & 22 deletions src/vcspull/_internal/config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import yaml

FormatLiteral = t.Literal["json", "yaml"]
RawConfigData: t.TypeAlias = dict[t.Any, t.Any]
RawConfigData: t.TypeAlias = dict[str, object]


class ConfigReader:
Expand All @@ -25,7 +25,7 @@ def __init__(self, content: RawConfigData) -> None:
self.content = content

@staticmethod
def _load(fmt: FormatLiteral, content: str) -> dict[str, t.Any]:
def _load(fmt: FormatLiteral, content: str) -> dict[str, object]:
"""Load raw config data and directly return it.

>>> ConfigReader._load("json", '{ "session_name": "my session" }')
Expand All @@ -36,14 +36,14 @@ def _load(fmt: FormatLiteral, content: str) -> dict[str, t.Any]:
"""
if fmt == "yaml":
return t.cast(
"dict[str, t.Any]",
"dict[str, object]",
yaml.load(
content,
Loader=yaml.SafeLoader,
),
)
if fmt == "json":
return t.cast("dict[str, t.Any]", json.loads(content))
return t.cast("dict[str, object]", json.loads(content))
msg = f"{fmt} not supported in configuration"
raise NotImplementedError(msg)

Expand Down Expand Up @@ -71,7 +71,7 @@ def load(cls, fmt: FormatLiteral, content: str) -> ConfigReader:
)

@classmethod
def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
def _from_file(cls, path: pathlib.Path) -> dict[str, object]:
r"""Load data from file path directly to dictionary.

**YAML file**
Expand Down Expand Up @@ -175,7 +175,7 @@ def _dump(
fmt: FormatLiteral,
content: RawConfigData,
indent: int = 2,
**kwargs: t.Any,
**kwargs: object,
) -> str:
r"""Dump directly.

Expand All @@ -200,7 +200,7 @@ def _dump(
msg = f"{fmt} not supported in config"
raise NotImplementedError(msg)

def dump(self, fmt: FormatLiteral, indent: int = 2, **kwargs: t.Any) -> str:
def dump(self, fmt: FormatLiteral, indent: int = 2, **kwargs: object) -> str:
r"""Dump via ConfigReader instance.

>>> cfg = ConfigReader({ "session_name": "my session" })
Expand All @@ -222,23 +222,23 @@ class _DuplicateTrackingSafeLoader(yaml.SafeLoader):

def __init__(self, stream: str) -> None:
super().__init__(stream)
self.top_level_key_values: dict[t.Any, list[t.Any]] = {}
self.top_level_key_values: dict[object, list[object]] = {}
self._mapping_depth = 0
self.top_level_items: list[tuple[t.Any, t.Any]] = []
self.top_level_items: list[tuple[object, object]] = []


def _duplicate_tracking_construct_mapping(
loader: _DuplicateTrackingSafeLoader,
node: yaml.nodes.MappingNode,
deep: bool = False,
) -> dict[t.Any, t.Any]:
) -> dict[object, object]:
loader._mapping_depth += 1
loader.flatten_mapping(node)
mapping: dict[t.Any, t.Any] = {}
mapping: dict[object, object] = {}

for key_node, value_node in node.value:
construct = t.cast(
"t.Callable[[yaml.nodes.Node], t.Any]",
"t.Callable[[yaml.nodes.Node], object]",
loader.construct_object,
)
key = construct(key_node)
Expand Down Expand Up @@ -268,28 +268,28 @@ def __init__(
self,
content: RawConfigData,
*,
duplicate_sections: dict[str, list[t.Any]] | None = None,
top_level_items: list[tuple[str, t.Any]] | None = None,
duplicate_sections: dict[str, list[object]] | None = None,
top_level_items: list[tuple[str, object]] | None = None,
) -> None:
super().__init__(content)
self._duplicate_sections = duplicate_sections or {}
self._top_level_items = top_level_items or []

@property
def duplicate_sections(self) -> dict[str, list[t.Any]]:
def duplicate_sections(self) -> dict[str, list[object]]:
"""Mapping of top-level keys to the list of duplicated values."""
return self._duplicate_sections

@property
def top_level_items(self) -> list[tuple[str, t.Any]]:
def top_level_items(self) -> list[tuple[str, object]]:
"""Ordered list of top-level items, including duplicates."""
return copy.deepcopy(self._top_level_items)

@classmethod
def _load_yaml_with_duplicates(
cls,
content: str,
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]], list[tuple[str, t.Any]]]:
) -> tuple[dict[str, object], dict[str, list[object]], list[tuple[str, object]]]:
loader = _DuplicateTrackingSafeLoader(content)

try:
Expand All @@ -299,12 +299,12 @@ def _load_yaml_with_duplicates(
dispose()

if data is None:
loaded: dict[str, t.Any] = {}
loaded: dict[str, object] = {}
else:
if not isinstance(data, dict):
msg = "Loaded configuration is not a mapping"
raise TypeError(msg)
loaded = t.cast("dict[str, t.Any]", data)
loaded = t.cast("dict[str, object]", data)

duplicate_sections = {
t.cast("str", key): values
Expand All @@ -323,7 +323,7 @@ def _load_yaml_with_duplicates(
def _load_from_path(
cls,
path: pathlib.Path,
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]], list[tuple[str, t.Any]]]:
) -> tuple[dict[str, object], dict[str, list[object]], list[tuple[str, object]]]:
if path.suffix.lower() in {".yaml", ".yml"}:
content = path.read_text(encoding="utf-8")
return cls._load_yaml_with_duplicates(content)
Expand All @@ -340,14 +340,14 @@ def from_file(cls, path: pathlib.Path) -> DuplicateAwareConfigReader:
)

@classmethod
def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
def _from_file(cls, path: pathlib.Path) -> dict[str, object]:
content, _, _ = cls._load_from_path(path)
return content

@classmethod
def load_with_duplicates(
cls,
path: pathlib.Path,
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]], list[tuple[str, t.Any]]]:
) -> tuple[dict[str, object], dict[str, list[object]], list[tuple[str, object]]]:
reader = cls.from_file(path)
return reader.content, reader.duplicate_sections, reader.top_level_items
6 changes: 5 additions & 1 deletion src/vcspull/_internal/private_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ class PrivatePath(PrivatePathBase):
'~/notes.txt'
"""

def __new__(cls, *args: t.Any, **kwargs: t.Any) -> PrivatePath:
def __new__(
cls,
*args: str | os.PathLike[str],
**kwargs: object,
) -> PrivatePath:
return super().__new__(cls, *args, **kwargs)

@classmethod
Expand Down
14 changes: 12 additions & 2 deletions src/vcspull/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
log = logging.getLogger(__name__)


SubparserTuple: t.TypeAlias = tuple[
argparse.ArgumentParser,
argparse.ArgumentParser,
argparse.ArgumentParser,
argparse.ArgumentParser,
argparse.ArgumentParser,
argparse.ArgumentParser,
]


def build_description(
intro: str,
example_blocks: t.Sequence[tuple[str | None, t.Sequence[str]]],
Expand Down Expand Up @@ -208,7 +218,7 @@ def build_description(
@overload
def create_parser(
return_subparsers: t.Literal[True],
) -> tuple[argparse.ArgumentParser, t.Any]: ...
) -> tuple[argparse.ArgumentParser, SubparserTuple]: ...


@overload
Expand All @@ -217,7 +227,7 @@ def create_parser(return_subparsers: t.Literal[False]) -> argparse.ArgumentParse

def create_parser(
return_subparsers: bool = False,
) -> argparse.ArgumentParser | tuple[argparse.ArgumentParser, t.Any]:
) -> argparse.ArgumentParser | tuple[argparse.ArgumentParser, SubparserTuple]:
"""Create CLI argument parser for vcspull."""
parser = argparse.ArgumentParser(
prog="vcspull",
Expand Down
14 changes: 12 additions & 2 deletions src/vcspull/cli/_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,21 @@
}


class _HelpTheme(t.Protocol):
heading: str
reset: str
label: str
long_option: str
short_option: str
prog: str
action: str


class VcspullHelpFormatter(argparse.RawDescriptionHelpFormatter):
"""Render description blocks while colorizing example sections when possible."""

def _fill_text(self, text: str, width: int, indent: str) -> str:
theme = getattr(self, "_theme", None)
theme = t.cast("_HelpTheme | None", getattr(self, "_theme", None))
if not text or theme is None:
return super()._fill_text(text, width, indent)

Expand Down Expand Up @@ -93,7 +103,7 @@ def _colorize_example_line(
self,
content: str,
*,
theme: t.Any,
theme: _HelpTheme,
expect_value: bool,
) -> _ColorizedLine:
parts: list[str] = []
Expand Down
Loading