pyOpenSourceProjects 0.5.1__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/AGENTS.md +28 -3
  2. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/PKG-INFO +2 -1
  3. pyopensourceprojects-0.6.0/osprojects/__init__.py +1 -0
  4. pyopensourceprojects-0.6.0/osprojects/git_api.py +79 -0
  5. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/osprojects/github_api.py +28 -28
  6. pyopensourceprojects-0.6.0/osprojects/gitlab_api.py +44 -0
  7. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/osprojects/osproject.py +82 -17
  8. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/osprojects/version.py +2 -1
  9. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/pyproject.toml +2 -0
  10. pyopensourceprojects-0.6.0/tests/test_gitlog2wiki.py +122 -0
  11. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/tests/test_osproject.py +1 -1
  12. pyopensourceprojects-0.5.1/osprojects/__init__.py +0 -1
  13. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/.github/workflows/build.yml +0 -0
  14. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/.github/workflows/upload-to-pypi.yml +0 -0
  15. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/.gitignore +0 -0
  16. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/.project +0 -0
  17. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/.pydevproject +0 -0
  18. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/LICENSE +0 -0
  19. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/README.md +0 -0
  20. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/mkdocs.yml +0 -0
  21. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/osprojects/check_project.py +0 -0
  22. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/osprojects/checkos.py +0 -0
  23. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/osprojects/editor.py +0 -0
  24. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/scripts/blackisort +0 -0
  25. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/scripts/doc +0 -0
  26. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/scripts/install +0 -0
  27. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/scripts/installAndTest +0 -0
  28. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/scripts/release +0 -0
  29. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/scripts/test +0 -0
  30. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/tests/__init__.py +0 -0
  31. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/tests/basetest.py +0 -0
  32. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/tests/test_github.py +0 -0
  33. {pyopensourceprojects-0.5.1 → pyopensourceprojects-0.6.0}/tests/test_github_api.py +0 -0
@@ -162,7 +162,32 @@ except requests.RequestException as e:
162
162
  def process_items(items: list[dict], filter_key: str) -> Optional[list[str]]:
163
163
  """Process items and return filtered values."""
164
164
  values = [item.get(filter_key) for item in items if filter_key in item]
165
- return values if values else None
165
+ result = values if values else None
166
+ return result
167
+ ```
168
+
169
+ ### Return Style
170
+
171
+ - Never return expressions directly; always assign to a variable first
172
+ - Avoid multiple return statements; use a single `return` at the end
173
+ - Use a result variable and set it conditionally:
174
+ ```python
175
+ # correct
176
+ def get_value(flag: bool) -> Optional[str]:
177
+ result = None
178
+ if flag:
179
+ result = "yes"
180
+ return result
181
+
182
+ # wrong - return expression
183
+ def get_value(flag: bool) -> Optional[str]:
184
+ return "yes" if flag else None
185
+
186
+ # wrong - multiple returns
187
+ def get_value(flag: bool) -> Optional[str]:
188
+ if not flag:
189
+ return None
190
+ return "yes"
166
191
  ```
167
192
 
168
193
  ## Version Bumping
@@ -187,12 +212,12 @@ Then run formatters, tests, commit and push (or use `scripts/release`).
187
212
 
188
213
  ## Git Workflow
189
214
 
190
- 1. Create a feature branch
215
+ 1. Read issue to work on
191
216
  2. Make changes following the code style guidelines
192
217
  3. Run formatters: `./scripts/blackisort`
193
218
  4. Run tests: `./scripts/test`
194
219
  5. Commit with a descriptive message
195
- 6. Push and create a pull request
220
+ 6. Push
196
221
 
197
222
  ## Release
198
223
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyOpenSourceProjects
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Check python OpenSource Projects to follow standards for README, github workflow and pyprojec.toml - creates badges
5
5
  Project-URL: Home, https://github.com/WolfgangFahl/pyOpenSourceProjects
6
6
  Project-URL: Documentation, http://wiki.bitplan.com/index.php/pyOpenSourceProjects
@@ -20,6 +20,7 @@ Requires-Dist: beautifulsoup4>=4.14.2
20
20
  Requires-Dist: gitpython
21
21
  Requires-Dist: packaging>=24.1
22
22
  Requires-Dist: py-3rdparty-mediawiki>=0.18.1
23
+ Requires-Dist: pybasemkit>=0.2.2
23
24
  Requires-Dist: pylodstorage>=0.17.0
24
25
  Requires-Dist: python-dateutil>=2.8.2
25
26
  Requires-Dist: ratelimit>=2.2.1
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
@@ -0,0 +1,79 @@
1
+ """Created on 2026-03-16.
2
+
3
+ @author: wf
4
+ """
5
+
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Dict, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class GenericRepo:
13
+ """Represents a generic git repository hosted on any forge.
14
+
15
+ Parses owner and project_id from any standard git remote URL
16
+ (SSH: git@host:owner/repo.git or HTTPS: https://host/owner/repo.git).
17
+
18
+ Attributes:
19
+ owner (str): The owner of the repository.
20
+ project_id (str): The name/id of the repository.
21
+ url (str): The original remote URL.
22
+ """
23
+
24
+ owner: str
25
+ project_id: str
26
+ url: str
27
+
28
+ @classmethod
29
+ def from_url(cls, url: str) -> Optional["GenericRepo"]:
30
+ """Parse owner and project_id from any standard git remote URL.
31
+
32
+ Args:
33
+ url: SSH or HTTPS git remote URL.
34
+
35
+ Returns:
36
+ GenericRepo instance or None if the URL cannot be parsed.
37
+ """
38
+ # Match HTTPS: https://host/owner/repo
39
+ https_pattern = r"https?://[^/]+/(?P<owner>[^/]+)/(?P<project_id>[^/.]+)"
40
+ match = re.match(https_pattern, url)
41
+
42
+ if not match:
43
+ # Match SSH: [user@]host:path - extract last two path components as owner/repo
44
+ ssh_pattern = r"(?:[^@]+@)?[^:]+:(?P<path>.+?)(?:\.git)?$"
45
+ ssh_match = re.match(ssh_pattern, url)
46
+ if ssh_match:
47
+ path = ssh_match.group("path")
48
+ # Split path and take last two components
49
+ parts = [p for p in path.split("/") if p]
50
+ if len(parts) >= 2:
51
+ owner = parts[-2]
52
+ project_id = parts[-1]
53
+ repo = cls(owner=owner, project_id=project_id, url=url)
54
+ return repo
55
+
56
+ repo = None
57
+ if match:
58
+ repo = cls(
59
+ owner=match.group("owner"),
60
+ project_id=match.group("project_id"),
61
+ url=url,
62
+ )
63
+ return repo
64
+
65
+ def projectUrl(self) -> str:
66
+ """Return a browsable HTTPS project URL derived from the remote URL."""
67
+ # Match SSH URLs: user@host:path or host:path
68
+ ssh = re.match(r"(?:[^@]+@)?(?P<host>[^:]+):(?P<path>.+?)(?:\.git)?$", self.url)
69
+ if ssh:
70
+ url = f"https://{ssh.group('host')}/{ssh.group('path')}"
71
+ else:
72
+ url = re.sub(r"\.git$", "", self.url)
73
+ return url
74
+
75
+ def getIssueRecords(self, limit: int = None, **params) -> List[Dict]:
76
+ """Not implemented for generic repos."""
77
+ raise NotImplementedError(
78
+ f"getIssueRecords is not supported for generic repo '{self.projectUrl()}'"
79
+ )
@@ -25,6 +25,8 @@ from backoff import expo, on_exception
25
25
  from basemkit.yamlable import lod_storable
26
26
  from ratelimit import RateLimitException, limits
27
27
 
28
+ from osprojects.git_api import GenericRepo
29
+
28
30
 
29
31
  class GitHubApi:
30
32
  """
@@ -96,9 +98,8 @@ class GitHubApi:
96
98
 
97
99
  if response.status_code == 302 and not allow_redirects:
98
100
  # Return the redirect URL if we're not following redirects
99
- return response.headers["Location"]
100
-
101
- if response.status_code not in [200, 302]:
101
+ result = response.headers["Location"]
102
+ elif response.status_code not in [200, 302]:
102
103
  err_msg = (
103
104
  f"Failed to {title} for {url}: {response.status_code} - {response.text}"
104
105
  )
@@ -106,8 +107,9 @@ class GitHubApi:
106
107
  if response.status_code in [403, 429]:
107
108
  raise RateLimitException(err_msg, period_remaining=60)
108
109
  raise Exception(err_msg)
109
-
110
- return response
110
+ else:
111
+ result = response
112
+ return result
111
113
 
112
114
  def repos_for_owner(self, owner: str, cache_expiry: int = 300) -> list[dict]:
113
115
  """Retrieve all repositories for the given owner, using cache if
@@ -130,14 +132,14 @@ class GitHubApi:
130
132
  if cache_content is not None and (
131
133
  cache_age is None or cache_age < cache_expiry
132
134
  ):
133
- return cache_content
134
-
135
- # If cache is not available or expired, retrieve from API
136
- repos = self.repos_for_owner_via_api(owner)
135
+ repos = cache_content
136
+ else:
137
+ # If cache is not available or expired, retrieve from API
138
+ repos = self.repos_for_owner_via_api(owner)
137
139
 
138
- # Cache the result
139
- with open(cache_file, "w") as f:
140
- json.dump(repos, f)
140
+ # Cache the result
141
+ with open(cache_file, "w") as f:
142
+ json.dump(repos, f)
141
143
 
142
144
  return repos
143
145
 
@@ -200,28 +202,28 @@ class GitHubApi:
200
202
 
201
203
 
202
204
  @dataclass
203
- class GitHubRepo:
205
+ class GitHubRepo(GenericRepo):
204
206
  """Represents a GitHub Repository.
205
207
 
206
208
  Attributes:
207
209
  owner (str): The owner of the repository.
208
210
  project_id (str): The name/id of the repository.
211
+ url (str): The original remote URL.
209
212
  """
210
213
 
211
- owner: str
212
- project_id: str
213
-
214
214
  def __post_init__(self):
215
215
  self.github = GitHubApi.get_instance()
216
216
 
217
217
  @classmethod
218
- def from_url(cls, url: str) -> (str, str):
219
- """Resolve project url to owner and project name.
218
+ def from_url(cls, url: str) -> "GitHubRepo":
219
+ """Resolve GitHub project url to owner and project name.
220
+
221
+ Args:
222
+ url: SSH or HTTPS GitHub remote URL.
220
223
 
221
224
  Returns:
222
- (owner, project)
225
+ GitHubRepo instance or None if the URL cannot be parsed.
223
226
  """
224
- # https://www.rfc-editor.org/rfc/rfc3986#appendix-B
225
227
  pattern = r"((https?:\/\/github\.com\/)|(git@github\.com:))(?P<owner>[^/?#]+)\/(?P<project_id>[^\./?#]+)(\.git)?"
226
228
  match = re.match(pattern=pattern, string=url)
227
229
  repo = None
@@ -229,17 +231,13 @@ class GitHubRepo:
229
231
  owner = match.group("owner")
230
232
  project_id = match.group("project_id")
231
233
  if owner and project_id:
232
- repo = cls(owner=owner, project_id=project_id)
233
- else:
234
- pass
235
- else:
236
- pass
234
+ repo = cls(owner=owner, project_id=project_id, url=url)
237
235
  return repo
238
236
 
239
237
  def ticketUrl(self):
240
238
  return f"{self.github.api_url}/repos/{self.owner}/{self.project_id}/issues"
241
239
 
242
- def projectUrl(self):
240
+ def projectUrl(self) -> str:
243
241
  return f"https://github.com/{self.owner}/{self.project_id}"
244
242
 
245
243
  def getIssueRecords(self, limit: int = None, **params) -> List[Dict]:
@@ -306,7 +304,6 @@ class GitHubFileSet:
306
304
 
307
305
  # Deduplication check
308
306
  if sha and not sha in self._sha_set:
309
-
310
307
  # Extract fields
311
308
  repo_info = api_item.get("repository", {})
312
309
 
@@ -433,7 +430,10 @@ class GitHubAction:
433
430
  raise ValueError("Invalid GitHub Actions URL format")
434
431
 
435
432
  try:
436
- repo = GitHubRepo(owner=path_parts[1], project_id=path_parts[2])
433
+ owner = path_parts[1]
434
+ project_id = path_parts[2]
435
+ repo_url = f"https://github.com/{owner}/{project_id}"
436
+ repo = GitHubRepo(owner=owner, project_id=project_id, url=repo_url)
437
437
  return cls(repo=repo, run_id=int(path_parts[5]), job_id=int(path_parts[7]))
438
438
  except (IndexError, ValueError) as e:
439
439
  raise ValueError(f"Failed to parse GitHub Actions URL: {e}")
@@ -0,0 +1,44 @@
1
+ """Created on 2026-03-16.
2
+
3
+ @author: wf
4
+ """
5
+
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+ from osprojects.git_api import GenericRepo
11
+
12
+
13
+ @dataclass
14
+ class GitLabRepo(GenericRepo):
15
+ """Represents a GitLab repository.
16
+
17
+ Attributes:
18
+ owner (str): The owner/namespace of the repository.
19
+ project_id (str): The name/id of the repository.
20
+ url (str): The original remote URL.
21
+ """
22
+
23
+ @classmethod
24
+ def from_url(cls, url: str) -> Optional["GitLabRepo"]:
25
+ """Parse owner and project_id from a GitLab remote URL.
26
+
27
+ Args:
28
+ url: SSH or HTTPS GitLab remote URL.
29
+
30
+ Returns:
31
+ GitLabRepo instance or None if the URL cannot be parsed.
32
+ """
33
+ pattern = (
34
+ r"(?:https?://[^/]+/|git@[^:]+:)(?P<owner>[^/]+)/(?P<project_id>[^/.]+)"
35
+ )
36
+ match = re.match(pattern, url)
37
+ repo = None
38
+ if match:
39
+ repo = cls(
40
+ owner=match.group("owner"),
41
+ project_id=match.group("project_id"),
42
+ url=url,
43
+ )
44
+ return repo
@@ -13,10 +13,13 @@ import subprocess
13
13
  import sys
14
14
  from typing import Dict, Iterable, List, Optional, Set, Tuple
15
15
 
16
+ from basemkit.base_cmd import BaseCmd
16
17
  from dateutil.parser import parse
17
18
  from tqdm import tqdm
18
19
 
20
+ from osprojects.git_api import GenericRepo
19
21
  from osprojects.github_api import GitHubApi, GitHubRepo
22
+ from osprojects.gitlab_api import GitLabRepo
20
23
 
21
24
 
22
25
  class Ticket(object):
@@ -327,16 +330,24 @@ class OsProject:
327
330
  self.repo_info = None # might be fetched
328
331
  self.folder = None # set for local projects
329
332
  if owner and project_id:
330
- self.repo = GitHubRepo(owner=owner, project_id=project_id)
333
+ url = f"https://github.com/{owner}/{project_id}"
334
+ self.repo = GitHubRepo(owner=owner, project_id=project_id, url=url)
331
335
 
332
336
  @classmethod
333
337
  def fromUrl(cls, url: str) -> "OsProject":
334
- """Init OsProject from given url."""
338
+ """Init OsProject from given url.
339
+
340
+ Selects the appropriate repo class based on the remote URL host:
341
+ GitHubRepo for github.com, GitLabRepo for gitlab hosts,
342
+ GenericRepo for everything else.
343
+ """
344
+ os_project = cls()
335
345
  if "github.com" in url:
336
- os_project = cls()
337
346
  os_project.repo = GitHubRepo.from_url(url)
347
+ elif "gitlab" in url:
348
+ os_project.repo = GitLabRepo.from_url(url)
338
349
  else:
339
- raise Exception(f"url '{url}' is not a github.com url ")
350
+ os_project.repo = GenericRepo.from_url(url)
340
351
  return os_project
341
352
 
342
353
  @classmethod
@@ -501,22 +512,18 @@ class OsProject:
501
512
  "--no-pager",
502
513
  "log",
503
514
  "--reverse",
504
- r'--pretty=format:{"name":"%cn","date":"%cI","hash":"%h"}',
515
+ r'--pretty=format:{"name":"%cn","date":"%cI","hash":"%h","subject":"%s"}',
505
516
  ]
506
- gitLogCommitSubject = ["git", "log", "--format=%s", "-n", "1"]
507
517
  rawCommitLogs = subprocess.check_output(gitlogCmd).decode()
508
518
  for rawLog in rawCommitLogs.split("\n"):
519
+ if not rawLog.strip():
520
+ continue
509
521
  log = json.loads(rawLog)
510
522
  if log.get("date", None) is not None:
511
523
  log["date"] = datetime.datetime.fromisoformat(log["date"])
512
524
  log["project"] = self.project_id
513
525
  log["host"] = self.projectUrl()
514
526
  log["path"] = ""
515
- log["subject"] = subprocess.check_output(
516
- [*gitLogCommitSubject, log["hash"]]
517
- )[
518
- :-1
519
- ].decode() # seperate query to avoid json escaping issues
520
527
  commit = Commit()
521
528
  for k, v in log.items():
522
529
  setattr(commit, k, v)
@@ -524,15 +531,73 @@ class OsProject:
524
531
  return commits
525
532
 
526
533
 
534
+ class GitLog2WikiCmd(BaseCmd):
535
+ """Command line interface for gitlog2wiki."""
536
+
537
+ def add_arguments(self, parser):
538
+ """Add gitlog2wiki-specific arguments to the parser.
539
+
540
+ Args:
541
+ parser: The argument parser to extend.
542
+ """
543
+ super().add_arguments(parser)
544
+ parser.add_argument(
545
+ "--filter",
546
+ help="Filter commits by date prefix, e.g. 2026, 2026-03, 2026-03-28",
547
+ default=None,
548
+ )
549
+
550
+ def handle_args(self, args):
551
+ """Handle parsed arguments and run the command.
552
+
553
+ Args:
554
+ args: Parsed argument namespace.
555
+
556
+ Returns:
557
+ bool: True if handled.
558
+ """
559
+ handled = super().handle_args(args)
560
+ result = False
561
+ if handled:
562
+ result = True
563
+ else:
564
+ osProject = OsProject.fromRepo()
565
+ if osProject.repo is None:
566
+ try:
567
+ url = subprocess.check_output(
568
+ ["git", "config", "--get", "remote.origin.url"]
569
+ )
570
+ url = url.decode().strip("\n")
571
+ print(
572
+ f"Error: Could not parse git remote URL: {url}",
573
+ file=sys.stderr,
574
+ )
575
+ except subprocess.CalledProcessError:
576
+ print(
577
+ "Error: Not in a git repository or no remote.origin.url configured",
578
+ file=sys.stderr,
579
+ )
580
+ result = False
581
+ else:
582
+ commits = osProject.getCommits()
583
+ if args.filter:
584
+ date_filter = args.filter
585
+ commits = [
586
+ c for c in commits if str(c.date.date()).startswith(date_filter)
587
+ ]
588
+ print("\n".join([c.toWikiMarkup() for c in commits]))
589
+ result = True
590
+ return result
591
+
592
+
527
593
  def gitlog2wiki(_argv=None):
528
594
  """Cmdline interface to get gitlog entries in wiki markup."""
529
- parser = argparse.ArgumentParser(description="gitlog2wiki")
530
- if _argv:
531
- _args = parser.parse_args(args=_argv)
595
+ from osprojects.version import Version
532
596
 
533
- osProject = OsProject.fromRepo()
534
- commits = osProject.getCommits()
535
- print("\n".join([c.toWikiMarkup() for c in commits]))
597
+ argv = sys.argv[1:] if _argv is None else _argv
598
+ cmd = GitLog2WikiCmd(Version)
599
+ result = cmd.run(argv)
600
+ return result
536
601
 
537
602
 
538
603
  def main(_argv=None):
@@ -12,5 +12,6 @@ class Version(object):
12
12
  name = "pyOpenSourceProjects"
13
13
  version = osprojects.__version__
14
14
  date = "2024-07-31"
15
- updated = "2026-03-14" # keeping date; version now aligns to 0.5.1
15
+ updated = "2026-03-28"
16
16
  description = "Check python OpenSource Projects to follow standards for README, github workflow and pyproject.toml - creates badges"
17
+ doc_url = "http://wiki.bitplan.com/index.php/pyOpenSourceProjects"
@@ -16,6 +16,8 @@ readme = "README.md"
16
16
  license = "Apache-2.0"
17
17
 
18
18
  dependencies = [
19
+ # https://pypi.org/project/pybasemkit/
20
+ "pybasemkit>=0.2.2",
19
21
  # https://pypi.org/project/beautifulsoup4/
20
22
  "beautifulsoup4>=4.14.2",
21
23
  # https://pypi.org/project/GitPython/
@@ -0,0 +1,122 @@
1
+ """Created on 2026-03-28.
2
+
3
+ @author: wf
4
+ """
5
+
6
+ import datetime
7
+ import unittest
8
+
9
+ from osprojects.osproject import Commit, GitLog2WikiCmd, gitlog2wiki
10
+ from osprojects.version import Version
11
+ from tests.basetest import BaseTest
12
+
13
+
14
+ class TestGitLog2WikiFilter(BaseTest):
15
+ """Test the --filter option of gitlog2wiki."""
16
+
17
+ def _make_commit(self, date_iso: str) -> Commit:
18
+ """Create a Commit with the given ISO date string.
19
+
20
+ Args:
21
+ date_iso: ISO 8601 date string, e.g. '2026-03-28T10:00:00+00:00'.
22
+
23
+ Returns:
24
+ A Commit instance with date and hash set.
25
+ """
26
+ commit = Commit()
27
+ commit.date = datetime.datetime.fromisoformat(date_iso)
28
+ commit.hash = "abc1234"
29
+ commit.subject = "Test commit"
30
+ commit.name = "Tester"
31
+ commit.project = "testproject"
32
+ commit.host = "https://github.com/test/testproject"
33
+ commit.path = ""
34
+ return commit
35
+
36
+ def setUp(self, debug=False, profile=True):
37
+ """Set up sample commits spanning multiple years/months/days."""
38
+ super().setUp(debug=debug, profile=profile)
39
+ self.commits = [
40
+ self._make_commit("2024-12-01T08:00:00+00:00"),
41
+ self._make_commit("2025-06-15T12:00:00+00:00"),
42
+ self._make_commit("2026-03-01T09:00:00+00:00"),
43
+ self._make_commit("2026-03-28T14:30:00+00:00"),
44
+ self._make_commit("2026-11-05T17:00:00+00:00"),
45
+ ]
46
+
47
+ def _apply_filter(self, date_filter: str):
48
+ """Apply the same filter logic as GitLog2WikiCmd.handle_args.
49
+
50
+ Args:
51
+ date_filter: Date prefix string, e.g. '2026', '2026-03', '2026-03-28'.
52
+
53
+ Returns:
54
+ Filtered list of Commit objects.
55
+ """
56
+ result = [c for c in self.commits if str(c.date.date()).startswith(date_filter)]
57
+ return result
58
+
59
+ def test_filter_by_year(self):
60
+ """Filter by year should return all commits in that year."""
61
+ filtered = self._apply_filter("2026")
62
+ dates = [str(c.date.date()) for c in filtered]
63
+ self.assertEqual(3, len(filtered))
64
+ self.assertIn("2026-03-01", dates)
65
+ self.assertIn("2026-03-28", dates)
66
+ self.assertIn("2026-11-05", dates)
67
+
68
+ def test_filter_by_month(self):
69
+ """Filter by year-month should return only commits in that month."""
70
+ filtered = self._apply_filter("2026-03")
71
+ dates = [str(c.date.date()) for c in filtered]
72
+ self.assertEqual(2, len(filtered))
73
+ self.assertIn("2026-03-01", dates)
74
+ self.assertIn("2026-03-28", dates)
75
+
76
+ def test_filter_by_day(self):
77
+ """Filter by exact day should return only commits on that day."""
78
+ filtered = self._apply_filter("2026-03-28")
79
+ self.assertEqual(1, len(filtered))
80
+ self.assertEqual("2026-03-28", str(filtered[0].date.date()))
81
+
82
+ def test_filter_no_match(self):
83
+ """Filter with no matching commits should return an empty list."""
84
+ filtered = self._apply_filter("2023")
85
+ self.assertEqual(0, len(filtered))
86
+
87
+ def test_no_filter_returns_all(self):
88
+ """Without a filter all commits are returned."""
89
+ filtered = self.commits # no filter applied
90
+ self.assertEqual(5, len(filtered))
91
+
92
+ def test_gitlog2wiki_filter_cmdline(self):
93
+ """Test gitlog2wiki CLI with --filter passes through and filters
94
+ output."""
95
+ if self.inPublicCI():
96
+ return
97
+ output = self.captureOutput(gitlog2wiki, ["--filter", "2022"])
98
+ lines = [line for line in output.strip().split("\n") if line]
99
+ # The very first commit (hash 106254f) is from 2022-01-24
100
+ self.assertTrue(len(lines) >= 1)
101
+ for line in lines:
102
+ self.assertIn("2022", line)
103
+
104
+ def test_gitlog2wiki_no_filter_cmdline(self):
105
+ """Test gitlog2wiki CLI without --filter returns all commits."""
106
+ if self.inPublicCI():
107
+ return
108
+ output_all = self.captureOutput(gitlog2wiki, [])
109
+ output_filtered = self.captureOutput(gitlog2wiki, ["--filter", "2022"])
110
+ lines_all = [l for l in output_all.strip().split("\n") if l]
111
+ lines_filtered = [l for l in output_filtered.strip().split("\n") if l]
112
+ self.assertGreater(len(lines_all), len(lines_filtered))
113
+
114
+ def test_gitlog2wiki_cmd_class(self):
115
+ """Test GitLog2WikiCmd can be instantiated with Version."""
116
+ cmd = GitLog2WikiCmd(Version)
117
+ self.assertIsNotNone(cmd)
118
+ self.assertIsInstance(cmd, GitLog2WikiCmd)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ unittest.main()
@@ -76,7 +76,7 @@ class TestOsProject(BaseTest):
76
76
  return
77
77
  commit = self.getSampleById(Commit, "hash", "106254f")
78
78
  expectedCommitMarkup = commit.toWikiMarkup()
79
- output = self.captureOutput(gitlog2wiki)
79
+ output = self.captureOutput(gitlog2wiki, [])
80
80
  outputLines = output.split("\n")
81
81
  self.assertTrue(expectedCommitMarkup in outputLines)
82
82
 
@@ -1 +0,0 @@
1
- __version__ = "0.5.1"