Skip to content

Commit a264714

Browse files
authored
refactor(git): Use dataclass in some areas (#329)
- `GitRemote` and `GitStatus`: Move to https://docs.python.org/3/library/dataclasses.html - `extract_status()`: Move to `GitStatus.from_stdout`
2 parents f82b3d2 + 439dd07 commit a264714

File tree

4 files changed

+103
-86
lines changed

4 files changed

+103
-86
lines changed

CHANGES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ $ pip install --user --upgrade --pre libvcs
3232
- Rename `ProjectLoggingAdapter` to `CmdLoggingAdapter`
3333
- `CmdLoggingAdapter`: Rename `repo_name` param to `keyword`
3434
- `create_repo` -> `create_project`
35+
- `GitRemote` and `GitStatus`: Move to {func}`dataclasses.dataclass` ({issue}`#329`)
36+
- `extract_status()`: Move to `GitStatus.from_stdout` ({issue}`#329`)
3537

3638
### What's new
3739

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ def setup(app):
157157
]
158158

159159
intersphinx_mapping = {
160-
"py": ("https://docs.python.org/2", None),
161-
"pip": ("http://pip.readthedocs.io/en/latest/", None),
160+
"py": ("https://docs.python.org/3", None),
161+
"pip": ("https://pip.pypa.io/en/latest/", None),
162162
}
163163

164164

libvcs/projects/git.py

Lines changed: 83 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
- [`GitProject.get_revision`](libvcs.git.GitProject.get_revision)
1515
- [`GitProject.get_git_version`](libvcs.git.GitProject.get_git_version)
1616
""" # NOQA: E501
17+
import dataclasses
1718
import logging
1819
import pathlib
1920
import re
20-
from typing import Dict, NamedTuple, Optional, TypedDict, Union
21+
from typing import Dict, Optional, TypedDict, Union
2122
from urllib import parse as urlparse
2223

2324
from .. import exc
@@ -26,78 +27,101 @@
2627
logger = logging.getLogger(__name__)
2728

2829

29-
class GitRemote(NamedTuple):
30-
"""Structure containing git repo information.
30+
class GitRemoteDict(TypedDict):
31+
"""For use when hydrating GitProject via dict."""
32+
33+
fetch_url: str
34+
push_url: str
3135

32-
Supports `collections.namedtuple._asdict()`
33-
"""
36+
37+
@dataclasses.dataclass
38+
class GitRemote:
39+
"""Structure containing git working copy information."""
3440

3541
name: str
3642
fetch_url: str
3743
push_url: str
3844

45+
def to_dict(self):
46+
return dataclasses.asdict(self)
47+
48+
def to_tuple(self):
49+
return dataclasses.astuple(self)
50+
3951

40-
class GitStatus(NamedTuple):
52+
GitProjectRemoteDict = Dict[str, GitRemote]
53+
GitFullRemoteDict = Dict[str, GitRemoteDict]
54+
GitRemotesArgs = Union[None, GitFullRemoteDict, Dict[str, str]]
55+
56+
57+
@dataclasses.dataclass
58+
class GitStatus:
4159
branch_oid: Optional[str]
4260
branch_head: Optional[str]
4361
branch_upstream: Optional[str]
4462
branch_ab: Optional[str]
4563
branch_ahead: Optional[str]
4664
branch_behind: Optional[str]
4765

66+
def to_dict(self):
67+
return dataclasses.asdict(self)
4868

49-
def extract_status(value) -> GitStatus:
50-
"""Returns ``git status -sb --porcelain=2`` extracted to a dict
69+
def to_tuple(self):
70+
return dataclasses.astuple(self)
5171

52-
Returns
53-
-------
54-
Dictionary of git repo's status
55-
"""
56-
pattern = re.compile(
57-
r"""[\n\r]?
58-
(
59-
#
60-
\W+
61-
branch.oid\W+
62-
(?P<branch_oid>
63-
[a-f0-9]{40}
64-
)
65-
)?
66-
(
67-
#
68-
\W+
69-
branch.head
70-
[\W]+
71-
(?P<branch_head>
72-
.*
73-
)
72+
@classmethod
73+
def from_stdout(cls, value: str):
74+
"""Returns ``git status -sb --porcelain=2`` extracted to a dict
7475
75-
)?
76-
(
77-
#
78-
\W+
79-
branch.upstream
80-
[\W]+
81-
(?P<branch_upstream>
82-
.*
83-
)
84-
)?
85-
(
86-
#
87-
\W+
88-
branch.ab
89-
[\W]+
90-
(?P<branch_ab>
91-
\+(?P<branch_ahead>\d+)
92-
\W{1}
93-
\-(?P<branch_behind>\d+)
94-
)
95-
)?
96-
""",
97-
re.VERBOSE | re.MULTILINE,
98-
)
99-
matches = pattern.search(value)
100-
return GitStatus(**matches.groupdict())
76+
Returns
77+
-------
78+
Dictionary of git repo's status
79+
"""
80+
pattern = re.compile(
81+
r"""[\n\r]?
82+
(
83+
#
84+
\W+
85+
branch.oid\W+
86+
(?P<branch_oid>
87+
[a-f0-9]{40}
88+
)
89+
)?
90+
(
91+
#
92+
\W+
93+
branch.head
94+
[\W]+
95+
(?P<branch_head>
96+
.*
97+
)
98+
99+
)?
100+
(
101+
#
102+
\W+
103+
branch.upstream
104+
[\W]+
105+
(?P<branch_upstream>
106+
.*
107+
)
108+
)?
109+
(
110+
#
111+
\W+
112+
branch.ab
113+
[\W]+
114+
(?P<branch_ab>
115+
\+(?P<branch_ahead>\d+)
116+
\W{1}
117+
\-(?P<branch_behind>\d+)
118+
)
119+
)?
120+
""",
121+
re.VERBOSE | re.MULTILINE,
122+
)
123+
matches = pattern.search(value)
124+
return cls(**matches.groupdict())
101125

102126

103127
def convert_pip_url(pip_url: str) -> VCSLocation:
@@ -125,18 +149,6 @@ def convert_pip_url(pip_url: str) -> VCSLocation:
125149
return VCSLocation(url=url, rev=rev)
126150

127151

128-
class GitRemoteDict(TypedDict):
129-
"""For use when hydrating GitProject via dict."""
130-
131-
fetch_url: str
132-
push_url: str
133-
134-
135-
GitProjectRemoteDict = Dict[str, GitRemote]
136-
GitFullRemoteDict = Dict[str, GitRemoteDict]
137-
GitRemotesArgs = Union[None, GitFullRemoteDict, Dict[str, str]]
138-
139-
140152
class GitProject(BaseProject):
141153
bin_name = "git"
142154
schemes = ("git", "git+http", "git+https", "git+ssh", "git+git", "git+file")
@@ -487,7 +499,7 @@ def remotes(self, flat=False) -> Dict:
487499

488500
for remote_name in ret:
489501
remotes[remote_name] = (
490-
self.remote(remote_name) if flat else self.remote(remote_name)._asdict()
502+
self.remote(remote_name) if flat else self.remote(remote_name).to_dict()
491503
)
492504
return remotes
493505

@@ -581,7 +593,7 @@ def get_git_version(self) -> str:
581593
version = ""
582594
return ".".join(version.split(".")[:3])
583595

584-
def status(self) -> dict:
596+
def status(self):
585597
"""Retrieve status of project in dict format.
586598
587599
Wraps ``git status --sb --porcelain=2``. Does not include changed files, yet.
@@ -606,7 +618,7 @@ def status(self) -> dict:
606618
branch_behind='0'\
607619
)
608620
"""
609-
return extract_status(self.run(["status", "-sb", "--porcelain=2"]))
621+
return GitStatus.from_stdout(self.run(["status", "-sb", "--porcelain=2"]))
610622

611623
def get_current_remote_name(self) -> str:
612624
"""Retrieve name of the remote / upstream of currently checked out branch.

tests/projects/test_git.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
GitFullRemoteDict,
1717
GitProject,
1818
GitRemote,
19+
GitStatus,
1920
convert_pip_url as git_convert_pip_url,
20-
extract_status,
2121
)
2222
from libvcs.shortcuts import create_project_from_pip_url
2323

@@ -315,7 +315,7 @@ def test_remotes(
315315
expected_remote_name,
316316
expected_remote_url,
317317
expected_remote_url,
318-
) == git_repo.remote(expected_remote_name)
318+
) == git_repo.remote(expected_remote_name).to_tuple()
319319

320320

321321
@pytest.mark.parametrize(
@@ -493,7 +493,7 @@ def test_remotes_preserves_git_ssh(
493493
git_repo.set_remote(name=remote_name, url=remote_url)
494494

495495
assert (
496-
GitRemote(remote_name, remote_url, remote_url)._asdict()
496+
GitRemote(remote_name, remote_url, remote_url).to_dict()
497497
in git_repo.remotes().values()
498498
)
499499

@@ -553,9 +553,12 @@ def test_get_remotes(git_repo: GitProject):
553553
def test_set_remote(git_repo: GitProject, repo_name: str, new_repo_url: str):
554554
mynewremote = git_repo.set_remote(name=repo_name, url="file:///")
555555

556-
assert "file:///" in mynewremote, "set_remote returns remote"
556+
assert "file:///" in mynewremote.fetch_url, "set_remote returns remote"
557557

558-
assert "file:///" in git_repo.remote(name=repo_name), "remote returns remote"
558+
assert isinstance(
559+
git_repo.remote(name=repo_name), GitRemote
560+
), "remote() returns GitRemote"
561+
assert "file:///" in git_repo.remote(name=repo_name).fetch_url, "new value set"
559562

560563
assert "myrepo" in git_repo.remotes(), ".remotes() returns new remote"
561564

@@ -567,8 +570,8 @@ def test_set_remote(git_repo: GitProject, repo_name: str, new_repo_url: str):
567570

568571
mynewremote = git_repo.set_remote(name="myrepo", url=new_repo_url, overwrite=True)
569572

570-
assert new_repo_url in git_repo.remote(
571-
name="myrepo"
573+
assert (
574+
new_repo_url in git_repo.remote(name="myrepo").fetch_url
572575
), "Running remove_set should overwrite previous remote"
573576

574577

@@ -614,7 +617,7 @@ def test_get_current_remote_name(git_repo: GitProject):
614617
), "Should reflect new upstream branch (different branch)"
615618

616619

617-
def test_extract_status():
620+
def test_GitRemote_from_stdout():
618621
FIXTURE_A = textwrap.dedent(
619622
"""
620623
# branch.oid d4ccd4d6af04b53949f89fbf0cdae13719dc5a08
@@ -625,7 +628,7 @@ def test_extract_status():
625628
assert {
626629
"branch_oid": "d4ccd4d6af04b53949f89fbf0cdae13719dc5a08",
627630
"branch_head": "fix-current-remote-name",
628-
}.items() <= extract_status(FIXTURE_A)._asdict().items()
631+
}.items() <= GitStatus.from_stdout(FIXTURE_A).to_dict().items()
629632

630633

631634
@pytest.mark.parametrize(
@@ -671,9 +674,9 @@ def test_extract_status():
671674
],
672675
],
673676
)
674-
def test_extract_status_b(fixture: str, expected_result: dict):
677+
def test_GitRemote__from_stdout_b(fixture: str, expected_result: dict):
675678
assert (
676-
extract_status(textwrap.dedent(fixture))._asdict().items()
679+
GitStatus.from_stdout(textwrap.dedent(fixture)).to_dict().items()
677680
>= expected_result.items()
678681
)
679682

@@ -721,10 +724,10 @@ def test_extract_status_b(fixture: str, expected_result: dict):
721724
],
722725
],
723726
)
724-
def test_extract_status_c(fixture: str, expected_result: dict):
727+
def test_GitRemote__from_stdout_c(fixture: str, expected_result: dict):
725728
assert (
726729
expected_result.items()
727-
<= extract_status(textwrap.dedent(fixture))._asdict().items()
730+
<= GitStatus.from_stdout(textwrap.dedent(fixture)).to_dict().items()
728731
)
729732

730733

0 commit comments

Comments
 (0)