Skip to content

Commit e7c69e4

Browse files
committed
WIP: Updates for mypy and new libvcs versions
1 parent 0424e8b commit e7c69e4

File tree

9 files changed

+210
-135
lines changed

9 files changed

+210
-135
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
html_css_files = ["css/custom.css"]
6262
html_extra_path = ["manifest.json"]
6363
html_theme = "furo"
64-
html_theme_path = []
64+
html_theme_path: list = []
6565
html_theme_options = {
6666
"light_logo": "img/vcspull.svg",
6767
"dark_logo": "img/vcspull-dark.svg",

scripts/generate_gitlab.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,20 @@
5858
print("File %s not accesible" % (config_filename))
5959
sys.exit(1)
6060

61-
result = requests.get(
61+
response = requests.get(
6262
"%s/api/v4/groups/%s/projects" % (gitlab_host, gitlab_namespace),
6363
params={"include_subgroups": "true", "per_page": "100"},
6464
headers={"Authorization": "Bearer %s" % (gitlab_token)},
6565
)
6666

67-
if 200 != result.status_code:
68-
print("Error: ", result)
67+
if 200 != response.status_code:
68+
print("Error: ", response)
6969
sys.exit(1)
7070

7171
path_prefix = os.getcwd()
72-
config = {}
72+
config: dict = {}
7373

74-
for group in result.json():
74+
for group in response.json():
7575
url_to_repo = group["ssh_url_to_repo"].replace(":", "/")
7676
namespace_path = group["namespace"]["full_path"]
7777
reponame = group["path"]

src/vcspull/cli/sync.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import click.shell_completion
77
from click.shell_completion import CompletionItem
88

9-
from libvcs.shortcuts import create_project_from_pip_url
9+
from libvcs._internal.shortcuts import create_project
10+
from vcspull.types import ConfigDict
1011

1112
from ..config import filter_repos, find_config_files, load_configs
1213

@@ -21,13 +22,13 @@ def get_repo_completions(
2122
if ctx.params["config"] is None
2223
else load_configs(files=[ctx.params["config"]])
2324
)
24-
found_repos = []
25+
found_repos: list[ConfigDict] = []
2526
repo_terms = [incomplete]
2627

2728
for repo_term in repo_terms:
2829
dir, vcs_url, name = None, None, None
2930
if any(repo_term.startswith(n) for n in ["./", "/", "~", "$HOME"]):
30-
dir = repo_term
31+
dir = dir
3132
elif any(repo_term.startswith(n) for n in ["http", "git", "svn", "hg"]):
3233
vcs_url = repo_term
3334
else:
@@ -105,9 +106,11 @@ def update_repo(repo_dict):
105106
repo_dict = deepcopy(repo_dict)
106107
if "pip_url" not in repo_dict:
107108
repo_dict["pip_url"] = repo_dict.pop("url")
109+
if "url" not in repo_dict:
110+
repo_dict["url"] = repo_dict.pop("pip_url")
108111
repo_dict["progress_callback"] = progress_cb
109112

110-
r = create_project_from_pip_url(**repo_dict) # Creates the repo object
113+
r = create_project(**repo_dict) # Creates the repo object
111114
r.update_repo(set_remotes=True) # Creates repo if not exists and fetches
112115

113116
return r

src/vcspull/config.py

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
import kaptan
1515

16-
from libvcs.projects.git import GitRemote
16+
from libvcs._internal.types import StrPath
17+
from libvcs.sync.git import GitRemote
1718

1819
from . import exc
20+
from .types import ConfigDict
1921
from .util import get_config_dir, update_dict
2022

2123
log = logging.getLogger(__name__)
@@ -45,7 +47,7 @@ def expand_dir(
4547
return _dir
4648

4749

48-
def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
50+
def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[ConfigDict]:
4951
"""Return expanded configuration.
5052
5153
end-user configuration permit inline configuration shortcuts, expand to
@@ -62,11 +64,11 @@ def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
6264
-------
6365
list : List of normalized repository information
6466
"""
65-
configs = []
67+
configs: list[ConfigDict] = []
6668
for directory, repos in config.items():
6769
for repo, repo_data in repos.items():
6870

69-
conf = {}
71+
conf: ConfigDict = {}
7072

7173
"""
7274
repo_name: http://myrepo.com/repo.git
@@ -94,18 +96,26 @@ def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
9496
if "parent_dir" not in conf:
9597
conf["parent_dir"] = expand_dir(directory, cwd=cwd)
9698

97-
# repo_dir -> dir in libvcs 0.12.0b25
98-
if "repo_dir" in conf and "dir" not in conf:
99-
conf["dir"] = conf.pop("repo_dir")
100-
10199
if "dir" not in conf:
102-
conf["dir"] = expand_dir(conf["parent_dir"] / conf["name"], cwd)
100+
conf["dir"] = expand_dir(
101+
pathlib.Path(conf["parent_dir"]) / conf["name"], cwd
102+
)
103103

104104
if "remotes" in conf:
105105
for remote_name, url in conf["remotes"].items():
106-
conf["remotes"][remote_name] = GitRemote(
107-
name=remote_name, fetch_url=url, push_url=url
108-
)
106+
if isinstance(url, GitRemote):
107+
continue
108+
if isinstance(url, str):
109+
conf["remotes"][remote_name] = GitRemote(
110+
name=remote_name, fetch_url=url, push_url=url
111+
)
112+
elif isinstance(url, dict):
113+
assert "push_url" in url
114+
assert "fetch_url" in url
115+
conf["remotes"][remote_name] = GitRemote(
116+
name=remote_name, **url
117+
)
118+
109119
configs.append(conf)
110120

111121
return configs
@@ -192,12 +202,12 @@ def find_config_files(
192202
configs.extend(find_config_files(path, match, f))
193203
else:
194204
match = f"{match}.{filetype}"
195-
configs = path.glob(match)
205+
configs = list(path.glob(match))
196206

197207
return configs
198208

199209

200-
def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
210+
def load_configs(files: list[StrPath], cwd=pathlib.Path.cwd()):
201211
"""Return repos from a list of files.
202212
203213
Parameters
@@ -216,10 +226,11 @@ def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
216226
----
217227
Validate scheme, check for duplicate destinations, VCS urls
218228
"""
219-
repos = []
229+
repos: list[ConfigDict] = []
220230
for file in files:
221231
if isinstance(file, str):
222232
file = pathlib.Path(file)
233+
assert isinstance(file, pathlib.Path)
223234
ext = file.suffix.lstrip(".")
224235
conf = kaptan.Kaptan(handler=ext).import_config(str(file))
225236
newrepos = extract_repos(conf.export("dict"), cwd=cwd)
@@ -230,51 +241,49 @@ def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
230241

231242
dupes = detect_duplicate_repos(repos, newrepos)
232243

233-
if dupes:
244+
if len(dupes) > 0:
234245
msg = ("repos with same path + different VCS detected!", dupes)
235246
raise exc.VCSPullException(msg)
236247
repos.extend(newrepos)
237248

238249
return repos
239250

240251

241-
def detect_duplicate_repos(repos1: list[dict], repos2: list[dict]):
252+
ConfigDictTuple = tuple[ConfigDict, ConfigDict]
253+
254+
255+
def detect_duplicate_repos(
256+
config1: list[ConfigDict], config2: list[ConfigDict]
257+
) -> list[ConfigDictTuple]:
242258
"""Return duplicate repos dict if repo_dir same and vcs different.
243259
244260
Parameters
245261
----------
246-
repos1 : dict
247-
list of repo expanded dicts
262+
config1 : list[ConfigDict]
248263
249-
repos2 : dict
250-
list of repo expanded dicts
264+
config2 : list[ConfigDict]
251265
252266
Returns
253267
-------
254-
list of dict, or None
255-
Duplicate repos
268+
list[ConfigDictTuple]
269+
List of duplicate tuples
256270
"""
257-
dupes = []
258-
path_dupe_repos = []
259-
260-
curpaths = [r["dir"] for r in repos1]
261-
newpaths = [r["dir"] for r in repos2]
262-
path_duplicates = list(set(curpaths).intersection(newpaths))
271+
if not config1:
272+
return []
263273

264-
if not path_duplicates:
265-
return None
274+
dupes: list[ConfigDictTuple] = []
266275

267-
path_dupe_repos.extend(
268-
[r for r in repos2 if any(r["dir"] == p for p in path_duplicates)]
269-
)
276+
repo_dirs = {
277+
pathlib.Path(repo["parent_dir"]) / repo["name"]: repo for repo in config1
278+
}
279+
repo_dirs_2 = {
280+
pathlib.Path(repo["parent_dir"]) / repo["name"]: repo for repo in config2
281+
}
270282

271-
if not path_dupe_repos:
272-
return None
283+
for repo_dir, repo in repo_dirs.items():
284+
if repo_dir in repo_dirs_2:
285+
dupes.append((repo, repo_dirs_2[repo_dir]))
273286

274-
for n in path_dupe_repos:
275-
currepo = next((r for r in repos1 if r["dir"] == n["dir"]), None)
276-
if n["url"] != currepo["url"]:
277-
dupes += (n, currepo)
278287
return dupes
279288

280289

@@ -304,11 +313,11 @@ def in_dir(config_dir=None, extensions: list[str] = [".yml", ".yaml", ".json"]):
304313

305314

306315
def filter_repos(
307-
config: dict,
308-
dir: Union[pathlib.Path, None] = None,
316+
config: list[ConfigDict],
317+
dir: Union[pathlib.Path, Literal["*"], None] = None,
309318
vcs_url: Union[str, None] = None,
310319
name: Union[str, None] = None,
311-
):
320+
) -> list[ConfigDict]:
312321
"""Return a :py:obj:`list` list of repos from (expanded) config file.
313322
314323
dir, vcs_url and name all support fnmatch.
@@ -329,23 +338,31 @@ def filter_repos(
329338
list :
330339
Repos
331340
"""
332-
repo_list = []
341+
repo_list: list[ConfigDict] = []
333342

334343
if dir:
335-
repo_list.extend([r for r in config if fnmatch.fnmatch(r["parent_dir"], dir)])
344+
repo_list.extend(
345+
[r for r in config if fnmatch.fnmatch(str(r["parent_dir"]), str(dir))]
346+
)
336347

337348
if vcs_url:
338349
repo_list.extend(
339-
r for r in config if fnmatch.fnmatch(r.get("url", r.get("repo")), vcs_url)
350+
r
351+
for r in config
352+
if fnmatch.fnmatch(str(r.get("url", r.get("repo"))), vcs_url)
340353
)
341354

342355
if name:
343-
repo_list.extend([r for r in config if fnmatch.fnmatch(r.get("name"), name)])
356+
repo_list.extend(
357+
[r for r in config if fnmatch.fnmatch(str(r.get("name")), name)]
358+
)
344359

345360
return repo_list
346361

347362

348-
def is_config_file(filename: str, extensions: list[str] = [".yml", ".yaml", ".json"]):
363+
def is_config_file(
364+
filename: str, extensions: Union[list[str], str] = [".yml", ".yaml", ".json"]
365+
):
349366
"""Return True if file has a valid config file type.
350367
351368
Parameters

src/vcspull/types.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import typing as t
2+
3+
from libvcs._internal.types import StrPath, VCSLiteral
4+
from libvcs.sync.git import GitSyncRemoteDict
5+
6+
7+
class RawConfigDict(t.TypedDict):
8+
vcs: VCSLiteral
9+
name: str
10+
dir: StrPath
11+
url: str
12+
remotes: GitSyncRemoteDict
13+
14+
15+
RawConfigDir = dict[str, RawConfigDict]
16+
RawConfig = dict[str, RawConfigDir]
17+
18+
19+
class ConfigDict(t.TypedDict):
20+
vcs: VCSLiteral
21+
name: str
22+
dir: StrPath
23+
parent_dir: StrPath
24+
url: str
25+
remotes: GitSyncRemoteDict

tests/conftest.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import pathlib
33
import shutil
44
import textwrap
5+
import typing as t
56

67
import pytest
78

89
from libvcs._internal.run import run
9-
from libvcs.projects.git import GitProject
10-
from libvcs.shortcuts import create_project_from_pip_url
10+
from libvcs._internal.shortcuts import create_project
11+
from libvcs.sync.git import GitSync
1112

1213

1314
@pytest.fixture(autouse=True, scope="session")
@@ -67,7 +68,7 @@ def clean():
6768

6869
@pytest.fixture
6970
def git_repo_kwargs(repos_path: pathlib.Path, git_dummy_repo_dir):
70-
"""Return kwargs for :func:`create_project_from_pip_url`."""
71+
"""Return kwargs for :func:`create_project`."""
7172
return {
7273
"url": "git+file://" + git_dummy_repo_dir,
7374
"parent_dir": str(repos_path),
@@ -76,16 +77,26 @@ def git_repo_kwargs(repos_path: pathlib.Path, git_dummy_repo_dir):
7677

7778

7879
@pytest.fixture
79-
def git_repo(git_repo_kwargs) -> GitProject:
80+
def git_repo(git_repo_kwargs) -> GitSync:
8081
"""Create an git repository for tests. Return repo."""
81-
repo = create_project_from_pip_url(**git_repo_kwargs)
82+
repo = create_project(vcs="git", **git_repo_kwargs)
8283
repo.obtain(quiet=True)
8384
return repo
8485

8586

87+
class DummyRepoProtocol(t.Protocol):
88+
"""Callback for repo fixture factory."""
89+
90+
def __call__(self, repo_name: str, testfile_filename: str = ...) -> str:
91+
"""Callback signature for subprocess communication."""
92+
...
93+
94+
8695
@pytest.fixture
87-
def create_git_dummy_repo(repos_path: pathlib.Path) -> pathlib.Path:
88-
def fn(repo_name, testfile_filename="testfile.test"):
96+
def create_git_dummy_repo(
97+
repos_path: pathlib.Path,
98+
) -> t.Generator[DummyRepoProtocol, None, None]:
99+
def fn(repo_name: str, testfile_filename: str = "testfile.test"):
89100
repo_path = str(repos_path / repo_name)
90101

91102
run(["git", "init", repo_name], cwd=str(repos_path))
@@ -100,7 +111,9 @@ def fn(repo_name, testfile_filename="testfile.test"):
100111

101112

102113
@pytest.fixture
103-
def git_dummy_repo_dir(repos_path: pathlib.Path, create_git_dummy_repo):
114+
def git_dummy_repo_dir(
115+
repos_path: pathlib.Path, create_git_dummy_repo: DummyRepoProtocol
116+
):
104117
"""Create a git repo with 1 commit, used as a remote."""
105118
return create_git_dummy_repo("dummyrepo")
106119

0 commit comments

Comments
 (0)