Skip to content

Commit ee33a08

Browse files
committed
refactor: replace SubProject inheritance with has-a Fetcher composition
SubProject was an ABC mixing VCS-specific capabilities with domain orchestration. Archive fetching has no concept of branches/tags/revisions, so forcing it to implement VCS-shaped stubs was a design smell. New domain model: - Fetcher (Protocol): minimal common contract (fetch, freeze, wanted_version, latest_available_version, list_tool_info) - VcsFetcher(Fetcher, Protocol): VCS additions (branches, tags, revisions, browse_tree, patch_type) — git and svn only - AbstractVcsFetcher(ABC): shared latest_available_version + freeze logic, eliminating duplication between GitFetcher and SvnFetcher - ArchiveFetcher: implements Fetcher only — no VCS methods - SubProject: concrete domain aggregate composed with a Fetcher; single dispatch point via as_vcs() -> VcsFetcher | None All cyclomatic complexity kept ≤ 6. Pylint 10.00/10. All 569 unit tests pass. Feature tests pass for git; SVN failures are environmental (no svn). https://claude.ai/code/session_01BMSF8XFAxV6hABQgL7RZ3z
1 parent 7e372b5 commit ee33a08

16 files changed

Lines changed: 789 additions & 709 deletions

dfetch/commands/add.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@
2929
from dfetch.manifest.remote import Remote
3030
from dfetch.manifest.version import Version
3131
from dfetch.project import create_sub_project, create_super_project
32-
from dfetch.project.gitsubproject import GitSubProject
3332
from dfetch.project.subproject import SubProject
3433
from dfetch.project.superproject import SuperProject
35-
from dfetch.project.svnsubproject import SvnSubProject
3634
from dfetch.terminal import Entry, LsFunction
3735
from dfetch.terminal.tree_browser import (
3836
BrowserConfig,
@@ -86,9 +84,9 @@ def browse_tree(subproject: SubProject, version: str = "") -> Generator[LsFuncti
8684
Adds '.' as the first entry to allow selecting the repo root (which is
8785
treated as empty src).
8886
"""
89-
if isinstance(subproject, (GitSubProject, SvnSubProject)):
90-
remote = subproject.remote_repo
91-
with remote.browse_tree(version) as vcs_ls:
87+
vcs = subproject.as_vcs()
88+
if vcs is not None:
89+
with vcs.browse_tree(version) as vcs_ls:
9290

9391
def ls(path: str = "") -> list[Entry]:
9492
entries = [

dfetch/commands/environment.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import dfetch.commands.command
1818
from dfetch.log import get_logger
19-
from dfetch.project import SUPPORTED_SUBPROJECT_TYPES
19+
from dfetch.project import SUPPORTED_FETCHERS
2020

2121
logger = get_logger(__name__)
2222

@@ -37,5 +37,5 @@ def __call__(self, _: argparse.Namespace) -> None:
3737
logger.print_report_line(
3838
"platform", f"{platform.system()} {platform.release()}"
3939
)
40-
for project_type in SUPPORTED_SUBPROJECT_TYPES:
41-
project_type.list_tool_info()
40+
for fetcher_type in SUPPORTED_FETCHERS:
41+
fetcher_type.list_tool_info()

dfetch/commands/format_patch.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@
3434
import dfetch.project
3535
from dfetch.log import get_logger
3636
from dfetch.project import create_super_project
37-
from dfetch.project.gitsubproject import GitSubProject
3837
from dfetch.project.subproject import SubProject
39-
from dfetch.project.svnsubproject import SvnSubProject
4038
from dfetch.util.util import (
4139
catch_runtime_exceptions,
4240
check_no_path_traversal,
@@ -145,12 +143,6 @@ def __call__(self, args: argparse.Namespace) -> None:
145143

146144

147145
def _determine_target_patch_type(subproject: SubProject) -> PatchType:
148-
"""Determine the subproject type for the patch."""
149-
if isinstance(subproject, GitSubProject):
150-
required_type = PatchType.GIT
151-
elif isinstance(subproject, SvnSubProject):
152-
required_type = PatchType.SVN
153-
else:
154-
required_type = PatchType.PLAIN
155-
156-
return required_type
146+
"""Determine the patch format for *subproject*."""
147+
vcs = subproject.as_vcs()
148+
return vcs.patch_type() if vcs is not None else PatchType.PLAIN

dfetch/project/__init__.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,37 @@
77
from dfetch.log import get_logger
88
from dfetch.manifest.manifest import Manifest
99
from dfetch.manifest.parse import find_manifest
10-
from dfetch.project.archivesubproject import ArchiveSubProject
11-
from dfetch.project.gitsubproject import GitSubProject
10+
from dfetch.project.archivesubproject import ArchiveFetcher
11+
from dfetch.project.gitsubproject import GitFetcher
1212
from dfetch.project.gitsuperproject import GitSuperProject
1313
from dfetch.project.subproject import SubProject
1414
from dfetch.project.superproject import NoVcsSuperProject, SuperProject
15-
from dfetch.project.svnsubproject import SvnSubProject
15+
from dfetch.project.svnsubproject import SvnFetcher
1616
from dfetch.project.svnsuperproject import SvnSuperProject
1717
from dfetch.util.util import resolve_absolute_path
1818

19-
SUPPORTED_SUBPROJECT_TYPES: list[
20-
type[ArchiveSubProject] | type[GitSubProject] | type[SvnSubProject]
21-
] = [ArchiveSubProject, GitSubProject, SvnSubProject]
19+
_AnyFetcherType = type[ArchiveFetcher] | type[GitFetcher] | type[SvnFetcher]
20+
SUPPORTED_FETCHERS: list[_AnyFetcherType] = [ArchiveFetcher, GitFetcher, SvnFetcher]
2221
SUPPORTED_SUPERPROJECT_TYPES = [GitSuperProject, SvnSuperProject]
2322

23+
# Backward-compatible alias used by environment.py and any external callers.
24+
SUPPORTED_SUBPROJECT_TYPES = SUPPORTED_FETCHERS
25+
2426
logger = get_logger(__name__)
2527

2628

2729
def create_sub_project(
2830
project_entry: dfetch.manifest.project.ProjectEntry,
2931
) -> SubProject:
30-
"""Create a new SubProject based on a project from the manifest."""
31-
for project_type in SUPPORTED_SUBPROJECT_TYPES:
32-
if project_type.NAME == project_entry.vcs:
33-
return project_type(project_entry)
32+
"""Create a SubProject by selecting the appropriate fetcher for *project_entry*."""
33+
for fetcher_type in SUPPORTED_FETCHERS:
34+
if fetcher_type.NAME == project_entry.vcs:
35+
return SubProject(project_entry, fetcher_type(project_entry.remote_url))
3436

35-
for project_type in SUPPORTED_SUBPROJECT_TYPES:
36-
project = project_type(project_entry)
37+
for fetcher_type in SUPPORTED_FETCHERS:
38+
if fetcher_type.handles(project_entry.remote_url):
39+
return SubProject(project_entry, fetcher_type(project_entry.remote_url))
3740

38-
if project.check():
39-
return project
4041
raise RuntimeError("vcs type unsupported")
4142

4243

0 commit comments

Comments
 (0)