Skip to content

Commit 71912e2

Browse files
committed
refactor: Basic mypy, refactor for new libvcs
1 parent 146e222 commit 71912e2

File tree

11 files changed

+264
-155
lines changed

11 files changed

+264
-155
lines changed

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
sys.path.insert(0, str(cwd / "_ext"))
1313

1414
# package data
15-
about = {}
15+
about: dict[str, str] = {}
1616
with open(src_root / "vcspull" / "__about__.py") as fp:
1717
exec(fp.read(), about)
1818

@@ -60,7 +60,7 @@
6060
html_css_files = ["css/custom.css"]
6161
html_extra_path = ["manifest.json"]
6262
html_theme = "furo"
63-
html_theme_path = []
63+
html_theme_path: list = []
6464
html_theme_options = {
6565
"light_logo": "img/vcspull.svg",
6666
"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: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
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 libvcs.url import registry as url_tools
11+
from vcspull.types import ConfigDict
1012

1113
from ..config import filter_repos, find_config_files, load_configs
1214

@@ -21,13 +23,13 @@ def get_repo_completions(
2123
if ctx.params["config"] is None
2224
else load_configs(files=[ctx.params["config"]])
2325
)
24-
found_repos = []
26+
found_repos: list[ConfigDict] = []
2527
repo_terms = [incomplete]
2628

2729
for repo_term in repo_terms:
2830
dir, vcs_url, name = None, None, None
2931
if any(repo_term.startswith(n) for n in ["./", "/", "~", "$HOME"]):
30-
dir = repo_term
32+
dir = dir
3133
elif any(repo_term.startswith(n) for n in ["http", "git", "svn", "hg"]):
3234
vcs_url = repo_term
3335
else:
@@ -105,9 +107,21 @@ def update_repo(repo_dict):
105107
repo_dict = deepcopy(repo_dict)
106108
if "pip_url" not in repo_dict:
107109
repo_dict["pip_url"] = repo_dict.pop("url")
110+
if "url" not in repo_dict:
111+
repo_dict["url"] = repo_dict.pop("pip_url")
108112
repo_dict["progress_callback"] = progress_cb
109113

110-
r = create_project_from_pip_url(**repo_dict) # Creates the repo object
114+
if repo_dict.get("vcs") is None:
115+
vcs_matches = url_tools.registry.match(url=repo_dict["url"], is_explicit=True)
116+
117+
if len(vcs_matches) == 0:
118+
raise Exception(f"No vcs found for {repo_dict}")
119+
if len(vcs_matches) > 1:
120+
raise Exception(f"No exact matches for {repo_dict}")
121+
122+
repo_dict["vcs"] = vcs_matches[0].vcs
123+
124+
r = create_project(**repo_dict) # Creates the repo object
111125
r.update_repo(set_remotes=True) # Creates repo if not exists and fetches
112126

113127
return r

src/vcspull/config.py

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@
99
import logging
1010
import os
1111
import pathlib
12+
import typing as t
1213
from typing import Literal, Optional, Union
1314

1415
import kaptan
1516

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

1820
from . import exc
21+
from .types import ConfigDict, RawConfigDict
1922
from .util import get_config_dir, update_dict
2023

2124
log = logging.getLogger(__name__)
2225

26+
if t.TYPE_CHECKING:
27+
from typing_extensions import TypeGuard
28+
2329

2430
def expand_dir(
2531
_dir: pathlib.Path, cwd: pathlib.Path = pathlib.Path.cwd()
@@ -45,7 +51,7 @@ def expand_dir(
4551
return _dir
4652

4753

48-
def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
54+
def extract_repos(config: RawConfigDict, cwd=pathlib.Path.cwd()) -> list[ConfigDict]:
4955
"""Return expanded configuration.
5056
5157
end-user configuration permit inline configuration shortcuts, expand to
@@ -62,11 +68,11 @@ def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
6268
-------
6369
list : List of normalized repository information
6470
"""
65-
configs = []
71+
configs: list[ConfigDict] = []
6672
for directory, repos in config.items():
73+
assert isinstance(repos, dict)
6774
for repo, repo_data in repos.items():
68-
69-
conf = {}
75+
conf: dict = {}
7076

7177
"""
7278
repo_name: http://myrepo.com/repo.git
@@ -91,21 +97,36 @@ def extract_repos(config: dict, cwd=pathlib.Path.cwd()) -> list[dict]:
9197

9298
if "name" not in conf:
9399
conf["name"] = repo
94-
if "parent_dir" not in conf:
95-
conf["parent_dir"] = expand_dir(directory, cwd=cwd)
96-
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")
100100

101101
if "dir" not in conf:
102-
conf["dir"] = expand_dir(conf["parent_dir"] / conf["name"], cwd)
102+
conf["dir"] = expand_dir(
103+
pathlib.Path(expand_dir(pathlib.Path(directory), cwd=cwd))
104+
/ conf["name"],
105+
cwd,
106+
)
103107

104108
if "remotes" in conf:
109+
assert isinstance(conf["remotes"], dict)
105110
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-
)
111+
if isinstance(url, GitRemote):
112+
continue
113+
if isinstance(url, str):
114+
conf["remotes"][remote_name] = GitRemote(
115+
name=remote_name, fetch_url=url, push_url=url
116+
)
117+
elif isinstance(url, dict):
118+
assert "push_url" in url
119+
assert "fetch_url" in url
120+
conf["remotes"][remote_name] = GitRemote(
121+
name=remote_name, **url
122+
)
123+
124+
def is_valid_config_dict(val: t.Any) -> "TypeGuard[ConfigDict]":
125+
assert isinstance(val, dict)
126+
return True
127+
128+
assert is_valid_config_dict(conf)
129+
109130
configs.append(conf)
110131

111132
return configs
@@ -192,12 +213,12 @@ def find_config_files(
192213
configs.extend(find_config_files(path, match, f))
193214
else:
194215
match = f"{match}.{filetype}"
195-
configs = path.glob(match)
216+
configs = list(path.glob(match))
196217

197218
return configs
198219

199220

200-
def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
221+
def load_configs(files: list[StrPath], cwd=pathlib.Path.cwd()):
201222
"""Return repos from a list of files.
202223
203224
Parameters
@@ -216,10 +237,11 @@ def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
216237
----
217238
Validate scheme, check for duplicate destinations, VCS urls
218239
"""
219-
repos = []
240+
repos: list[ConfigDict] = []
220241
for file in files:
221242
if isinstance(file, str):
222243
file = pathlib.Path(file)
244+
assert isinstance(file, pathlib.Path)
223245
ext = file.suffix.lstrip(".")
224246
conf = kaptan.Kaptan(handler=ext).import_config(str(file))
225247
newrepos = extract_repos(conf.export("dict"), cwd=cwd)
@@ -230,51 +252,49 @@ def load_configs(files: list[Union[str, pathlib.Path]], cwd=pathlib.Path.cwd()):
230252

231253
dupes = detect_duplicate_repos(repos, newrepos)
232254

233-
if dupes:
255+
if len(dupes) > 0:
234256
msg = ("repos with same path + different VCS detected!", dupes)
235257
raise exc.VCSPullException(msg)
236258
repos.extend(newrepos)
237259

238260
return repos
239261

240262

241-
def detect_duplicate_repos(repos1: list[dict], repos2: list[dict]):
263+
ConfigDictTuple = tuple[ConfigDict, ConfigDict]
264+
265+
266+
def detect_duplicate_repos(
267+
config1: list[ConfigDict], config2: list[ConfigDict]
268+
) -> list[ConfigDictTuple]:
242269
"""Return duplicate repos dict if repo_dir same and vcs different.
243270
244271
Parameters
245272
----------
246-
repos1 : dict
247-
list of repo expanded dicts
273+
config1 : list[ConfigDict]
248274
249-
repos2 : dict
250-
list of repo expanded dicts
275+
config2 : list[ConfigDict]
251276
252277
Returns
253278
-------
254-
list of dict, or None
255-
Duplicate repos
279+
list[ConfigDictTuple]
280+
List of duplicate tuples
256281
"""
257-
dupes = []
258-
path_dupe_repos = []
282+
if not config1:
283+
return []
259284

260-
curpaths = [r["dir"] for r in repos1]
261-
newpaths = [r["dir"] for r in repos2]
262-
path_duplicates = list(set(curpaths).intersection(newpaths))
285+
dupes: list[ConfigDictTuple] = []
263286

264-
if not path_duplicates:
265-
return None
287+
repo_dirs = {
288+
pathlib.Path(repo["dir"]).parent / repo["name"]: repo for repo in config1
289+
}
290+
repo_dirs_2 = {
291+
pathlib.Path(repo["dir"]).parent / repo["name"]: repo for repo in config2
292+
}
266293

267-
path_dupe_repos.extend(
268-
[r for r in repos2 if any(r["dir"] == p for p in path_duplicates)]
269-
)
294+
for repo_dir, repo in repo_dirs.items():
295+
if repo_dir in repo_dirs_2:
296+
dupes.append((repo, repo_dirs_2[repo_dir]))
270297

271-
if not path_dupe_repos:
272-
return None
273-
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)
278298
return dupes
279299

280300

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

305325

306326
def filter_repos(
307-
config: dict,
308-
dir: Union[pathlib.Path, None] = None,
327+
config: list[ConfigDict],
328+
dir: Union[pathlib.Path, Literal["*"], None] = None,
309329
vcs_url: Union[str, None] = None,
310330
name: Union[str, None] = None,
311-
):
331+
) -> list[ConfigDict]:
312332
"""Return a :py:obj:`list` list of repos from (expanded) config file.
313333
314334
dir, vcs_url and name all support fnmatch.
@@ -329,23 +349,35 @@ def filter_repos(
329349
list :
330350
Repos
331351
"""
332-
repo_list = []
352+
repo_list: list[ConfigDict] = []
333353

334354
if dir:
335-
repo_list.extend([r for r in config if fnmatch.fnmatch(r["parent_dir"], dir)])
355+
repo_list.extend(
356+
[
357+
r
358+
for r in config
359+
if fnmatch.fnmatch(str(pathlib.Path(r["dir"]).parent), str(dir))
360+
]
361+
)
336362

337363
if vcs_url:
338364
repo_list.extend(
339-
r for r in config if fnmatch.fnmatch(r.get("url", r.get("repo")), vcs_url)
365+
r
366+
for r in config
367+
if fnmatch.fnmatch(str(r.get("url", r.get("repo"))), vcs_url)
340368
)
341369

342370
if name:
343-
repo_list.extend([r for r in config if fnmatch.fnmatch(r.get("name"), name)])
371+
repo_list.extend(
372+
[r for r in config if fnmatch.fnmatch(str(r.get("name")), name)]
373+
)
344374

345375
return repo_list
346376

347377

348-
def is_config_file(filename: str, extensions: list[str] = [".yml", ".yaml", ".json"]):
378+
def is_config_file(
379+
filename: str, extensions: Union[list[str], str] = [".yml", ".yaml", ".json"]
380+
):
349381
"""Return True if file has a valid config file type.
350382
351383
Parameters

src/vcspull/types.py

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

0 commit comments

Comments
 (0)