Skip to content

Commit 607a025

Browse files
committed
refactoring
1 parent 14e0236 commit 607a025

File tree

2 files changed

+187
-121
lines changed

2 files changed

+187
-121
lines changed

src/edit_python_pe/main.py

Lines changed: 163 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@
2222
class MemberApp(App):
2323
"""Single app that toggles between a file list and a form while connected to a GitHub fork+push flow."""
2424

25-
def __init__(self, repo_path: str) -> None:
25+
def __init__(
26+
self,
27+
original_repo: Repository,
28+
forked_repo: Repository,
29+
token: str,
30+
repo_path: str,
31+
) -> None:
2632
super().__init__()
33+
self.original_repo = original_repo
34+
self.forked_repo = forked_repo
35+
self.token = token
2736
self.repo_path = repo_path
2837

2938
def compose(self) -> ComposeResult:
@@ -476,112 +485,18 @@ def save_member(self) -> None:
476485

477486
md_content = "\n".join(md_lines)
478487

479-
if not self.current_file:
480-
# compute name_file
481-
if aliases:
482-
alias_for_name = aliases[0].lower().replace(" ", "_")
483-
else:
484-
alias_for_name = name.lower().replace(" ", "_")
485-
486-
sha_hash = hashlib.sha1(
487-
(alias_for_name + email + datetime.now().isoformat()).encode(
488-
"utf-8"
489-
)
490-
).hexdigest()[:8]
491-
name_file = f"{alias_for_name}-{sha_hash}"
492-
493-
# Write file
494-
file_path = os.path.join(
495-
self.repo_path, "blog", "members", f"{name_file}.md"
496-
)
497-
else:
498-
name_file = self.current_file
499-
file_path = os.path.join(
500-
self.repo_path, "blog", "members", f"{name_file}"
501-
)
502-
os.makedirs(os.path.dirname(file_path), exist_ok=True)
503-
with open(file_path, "w", encoding="utf-8") as f:
504-
f.write(md_content)
505-
506-
# commit & push
507-
repo = pygit2.Repository(self.repo_path)
508-
rel_path = os.path.relpath(file_path, self.repo_path)
509-
rel_path = pathlib.Path(
510-
rel_path
511-
).as_posix() # Force path to POSIX format so Windows backslashes (\) don't break pygit2
512-
repo.index.add(rel_path)
513-
repo.index.write()
514-
author_sig = pygit2.Signature(
515-
name or "Unknown", email or "unknown@email"
516-
)
517-
tree_id = repo.index.write_tree()
518-
parents = [] if repo.head_is_unborn else [repo.head.target]
519-
commit_msg = (
520-
f"Changed {self.current_file}"
521-
if self.current_file
522-
else f"Added {name_file}.md"
523-
)
524-
repo.create_commit(
525-
"HEAD", author_sig, author_sig, commit_msg, tree_id, parents
488+
message = create_pr(
489+
md_content,
490+
self.current_file,
491+
self.repo_path,
492+
self.original_repo,
493+
self.forked_repo,
494+
self.token,
495+
aliases,
496+
name,
497+
email,
526498
)
527-
528-
callbacks = pygit2.callbacks.RemoteCallbacks(
529-
credentials=pygit2.UserPass(self.token, "x-oauth-basic")
530-
)
531-
remote = repo.remotes["origin"]
532-
remote.push([repo.head.name], callbacks=callbacks)
533-
534-
# PR logic
535-
pr_title = commit_msg
536-
first_alias = aliases[0] if aliases else ""
537-
pr_body = (
538-
f"Changing an entry to `blog/members` for {name} (alias: {first_alias})."
539-
if self.current_file
540-
else f"Creating a new entry to `blog/members` for {name} (alias: {first_alias})."
541-
)
542-
fork_owner = self.forked_repo.owner.login
543-
head_branch = f"{fork_owner}:main"
544-
base_branch = "main"
545-
546-
# If editing, retrieve PR by title and push to its branch
547-
if self.current_file:
548-
# Try to find an open PR with matching title
549-
prs = self.original_repo.get_pulls(
550-
state="open", sort="created", base=base_branch
551-
)
552-
pr_found = None
553-
for pr in prs:
554-
if self.current_file in pr.title:
555-
pr_found = pr
556-
break
557-
if pr_found:
558-
# Push to the PR branch (simulate, as actual branch logic may differ)
559-
remote.push([repo.head.name], callbacks=callbacks)
560-
self.exit(
561-
message=f"Archivo {self.current_file} editado, commit y cambios enviados al PR existente."
562-
)
563-
else:
564-
# Otherwise, create a new PR
565-
self.original_repo.create_pull(
566-
title=pr_title,
567-
body=pr_body,
568-
head=head_branch,
569-
base=base_branch,
570-
)
571-
self.exit(
572-
message=f"Archivo {self.current_file} guardado, commit y PR listo."
573-
)
574-
else:
575-
# Otherwise, create a new PR
576-
self.original_repo.create_pull(
577-
title=pr_title,
578-
body=pr_body,
579-
head=head_branch,
580-
base=base_branch,
581-
)
582-
self.exit(
583-
message=f"Archivo {name_file}.md guardado, commit y PR listo."
584-
)
499+
self.exit(message=message)
585500

586501
async def on_event(self, event: Event) -> None:
587502
# catch listview selection
@@ -609,7 +524,7 @@ def get_repo() -> tuple[str, Repository]:
609524
exit(1)
610525

611526

612-
def fork_repo(token: str, original_repo: Repository) -> str:
527+
def fork_repo(token: str, original_repo: Repository) -> tuple[str, Repository]:
613528
forked_repo = original_repo.create_fork()
614529
forked_repo_url = forked_repo.clone_url
615530
repo_path = user_data_dir(appname="edit-python-pe", appauthor="python.pe")
@@ -622,13 +537,151 @@ def fork_repo(token: str, original_repo: Repository) -> str:
622537
pygit2.clone_repository(
623538
forked_repo_url, repo_path, callbacks=callbacks
624539
)
625-
return repo_path
540+
return repo_path, forked_repo
541+
542+
543+
def compute_file_name(aliases: list[str], name: str, email: str) -> str:
544+
# compute name_file
545+
if aliases:
546+
alias_for_name = aliases[0].lower().replace(" ", "_")
547+
else:
548+
alias_for_name = name.lower().replace(" ", "_")
549+
550+
sha_hash = hashlib.sha1(
551+
(alias_for_name + email + datetime.now().isoformat()).encode("utf-8")
552+
).hexdigest()[:8]
553+
return f"{alias_for_name}-{sha_hash}.md"
554+
555+
556+
def write_file(file_content: str, file_path: str) -> None:
557+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
558+
with open(file_path, "w", encoding="utf-8") as f:
559+
f.write(file_content)
560+
561+
562+
def commit_and_push(
563+
repo_path: str,
564+
token: str,
565+
was_changed: bool,
566+
name_file: str,
567+
file_path: str,
568+
name: str,
569+
email: str,
570+
) -> tuple[
571+
str,
572+
pygit2.repository.Repository,
573+
pygit2.remotes.Remote,
574+
pygit2.callbacks.RemoteCallbacks,
575+
]:
576+
repo = pygit2.repository.Repository(repo_path)
577+
rel_path = os.path.relpath(file_path, repo_path)
578+
rel_path = pathlib.Path(
579+
rel_path
580+
).as_posix() # Force path to POSIX format so Windows backslashes (\) don't break pygit2
581+
repo.index.add(rel_path)
582+
repo.index.write()
583+
author_sig = pygit2.Signature(name or "Unknown", email or "unknown@email")
584+
tree_id = repo.index.write_tree()
585+
parents = [] if repo.head_is_unborn else [repo.head.target]
586+
commit_msg = (
587+
f"Changed {name_file}" if was_changed else f"Added {name_file}"
588+
)
589+
repo.create_commit(
590+
"HEAD", author_sig, author_sig, commit_msg, tree_id, parents
591+
)
592+
593+
callbacks = pygit2.callbacks.RemoteCallbacks(
594+
credentials=pygit2.UserPass(token, "x-oauth-basic")
595+
)
596+
remote = repo.remotes["origin"]
597+
remote.push([repo.head.name], callbacks=callbacks)
598+
return commit_msg, repo, remote, callbacks
599+
600+
601+
def create_pr(
602+
file_content: str,
603+
current_file: str | None,
604+
repo_path: str,
605+
original_repo: Repository,
606+
forked_repo: Repository,
607+
token: str,
608+
aliases: list[str],
609+
name: str,
610+
email: str,
611+
) -> str:
612+
# Name the file
613+
name_file = (
614+
current_file
615+
if current_file is not None
616+
else compute_file_name(aliases, name, email)
617+
)
618+
619+
# Write file
620+
file_path = os.path.join(repo_path, "blog", "members", name_file)
621+
write_file(file_content, file_path)
622+
623+
# commit & push
624+
commit_msg, repo, remote, callbacks = commit_and_push(
625+
repo_path,
626+
token,
627+
current_file is not None,
628+
name_file,
629+
file_path,
630+
name,
631+
email,
632+
)
633+
634+
# PR logic
635+
pr_title = commit_msg
636+
first_alias = aliases[0] if aliases else ""
637+
pr_body = (
638+
f"Changing an entry to `blog/members` for {name} (alias: {first_alias})."
639+
if current_file
640+
else f"Creating a new entry to `blog/members` for {name} (alias: {first_alias})."
641+
)
642+
fork_owner = forked_repo.owner.login
643+
head_branch = f"{fork_owner}:main"
644+
base_branch = "main"
645+
646+
# If editing, retrieve PR by title and push to its branch
647+
if current_file:
648+
# Try to find an open PR with matching title
649+
prs = original_repo.get_pulls(
650+
state="open", sort="created", base=base_branch
651+
)
652+
pr_found = None
653+
for pr in prs:
654+
if current_file in pr.title:
655+
pr_found = pr
656+
break
657+
if pr_found:
658+
# Push to the PR branch (simulate, as actual branch logic may differ)
659+
remote.push([repo.head.name], callbacks=callbacks)
660+
return f"Archivo {name_file} editado, commit y cambios enviados al PR existente."
661+
else:
662+
# Otherwise, create a new PR
663+
original_repo.create_pull(
664+
title=pr_title,
665+
body=pr_body,
666+
head=head_branch,
667+
base=base_branch,
668+
)
669+
return f"Archivo {name_file} guardado, commit y PR listo."
670+
else:
671+
# Otherwise, create a new PR
672+
original_repo.create_pull(
673+
title=pr_title,
674+
body=pr_body,
675+
head=head_branch,
676+
base=base_branch,
677+
)
678+
return f"Archivo {name_file} guardado, commit y PR listo."
626679

627680

628681
def main() -> None:
629682
token, original_repo = get_repo()
630-
repo_path = fork_repo(token, original_repo)
631-
app = MemberApp(repo_path)
683+
repo_path, forked_repo = fork_repo(token, original_repo)
684+
app = MemberApp(original_repo, forked_repo, token, repo_path)
632685
app.run()
633686

634687

tests/test_member_app.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ def setUp(self):
1616
# Patch Github and Repository for testing
1717
self.token = "fake-token"
1818
self.repo = MagicMock()
19-
self.app = MemberApp(repo_path="test_repo")
19+
self.original_repo = MagicMock()
20+
self.forked_repo = MagicMock()
21+
self.app = MemberApp(
22+
repo_path="test_repo",
23+
original_repo=self.original_repo,
24+
forked_repo=self.forked_repo,
25+
token=self.token,
26+
)
2027
self.app.social_container = MagicMock()
2128
self.app.alias_container = MagicMock()
2229
self.app.list_container = MagicMock()
@@ -164,7 +171,7 @@ def test_save_member_edit_no_pr(self):
164171
with (
165172
patch("os.makedirs") as makedirs,
166173
patch("builtins.open", MagicMock()),
167-
patch("pygit2.Repository") as RepoMock,
174+
patch("pygit2.repository.Repository") as RepoMock,
168175
):
169176
repo_instance = RepoMock.return_value
170177
repo_instance.index.add = MagicMock()
@@ -221,7 +228,7 @@ def test_save_member_edit(self):
221228
with (
222229
patch("os.makedirs") as makedirs,
223230
patch("builtins.open", MagicMock()),
224-
patch("pygit2.Repository") as RepoMock,
231+
patch("pygit2.repository.Repository") as RepoMock,
225232
):
226233
repo_instance = RepoMock.return_value
227234
repo_instance.index.add = MagicMock()
@@ -277,7 +284,7 @@ def test_save_member_new(self):
277284
with (
278285
patch("os.makedirs") as makedirs,
279286
patch("builtins.open", MagicMock()),
280-
patch("pygit2.Repository") as RepoMock,
287+
patch("pygit2.repository.Repository") as RepoMock,
281288
):
282289
repo_instance = RepoMock.return_value
283290
repo_instance.index.add = MagicMock()
@@ -329,7 +336,7 @@ def test_save_member_error_handling(self):
329336
patch.object(app, "exit") as exit_mock,
330337
patch("os.makedirs"),
331338
patch("builtins.open", MagicMock()),
332-
patch("pygit2.Repository"),
339+
patch("pygit2.repository.Repository"),
333340
):
334341
# Leave name and email blank to trigger error
335342
app.name_input.value = ""
@@ -499,11 +506,12 @@ def test_fork_repo_clones_if_not_exists(
499506
mock_original_repo = MagicMock()
500507
mock_original_repo.create_fork.return_value = mock_forked_repo
501508
token = "fake-token"
502-
repo_path = fork_repo(token, mock_original_repo)
509+
repo_path = fork_repo(token, mock_original_repo)[0]
503510
mock_original_repo.create_fork.assert_called_once()
504-
mock_clone.assert_called_once_with(
505-
mock_forked_repo.clone_url, repo_path, callbacks=unittest.mock.ANY
506-
)
511+
mock_clone.assert_called_once()
512+
call_args = mock_clone.call_args
513+
self.assertEqual(call_args[0][0], mock_forked_repo.clone_url)
514+
self.assertEqual(call_args[0][1], repo_path)
507515
self.assertEqual(repo_path, "/tmp/testrepo")
508516

509517
@patch("edit_python_pe.main.user_data_dir", return_value="/tmp/testrepo")
@@ -523,11 +531,16 @@ def test_main_runs_app(
523531
self, mock_member_app, mock_fork_repo, mock_get_repo
524532
):
525533
mock_get_repo.return_value = ("token", MagicMock())
526-
mock_fork_repo.return_value = "/tmp/testrepo"
534+
mock_fork_repo.return_value = ("/tmp/testrepo", MagicMock())
527535
mock_app_instance = MagicMock()
528536
mock_member_app.return_value = mock_app_instance
529537
main()
530538
mock_get_repo.assert_called_once()
531539
mock_fork_repo.assert_called_once()
532-
mock_member_app.assert_called_once_with("/tmp/testrepo")
540+
mock_member_app.assert_called_once_with(
541+
unittest.mock.ANY,
542+
unittest.mock.ANY,
543+
unittest.mock.ANY,
544+
unittest.mock.ANY,
545+
)
533546
mock_app_instance.run.assert_called_once()

0 commit comments

Comments
 (0)