package-dev-tools 0.8.4__tar.gz → 0.8.6__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 (54) hide show
  1. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/PKG-INFO +8 -7
  2. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/README.md +1 -1
  3. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/pyproject.toml +9 -6
  4. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/instantiate_new_project/instantiate_new_project.py +3 -2
  5. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/instantiate_new_project/substitute_template_name/substitute_template_name.py +2 -9
  6. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/template_sync/merge.py +4 -10
  7. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/template_sync/sync.py +2 -5
  8. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/extract_info.py +2 -2
  9. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/models/path.py +1 -4
  10. package_dev_tools-0.8.6/src/package_dev_tools/pre_commit/check_coverage.py +55 -0
  11. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/pre_commit/check_import.py +2 -3
  12. package_dev_tools-0.8.6/src/package_dev_tools/pre_commit/check_shields.py +25 -0
  13. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/utils/badge.py +1 -0
  14. {package_dev_tools-0.8.4/src/package_dev_tools/actions/instantiate_new_project → package_dev_tools-0.8.6/src/package_dev_tools/utils}/git.py +12 -3
  15. package_dev_tools-0.8.6/src/package_dev_tools/utils/package.py +50 -0
  16. package_dev_tools-0.8.6/src/package_dev_tools/utils/python_versions.py +77 -0
  17. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools.egg-info/PKG-INFO +8 -7
  18. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools.egg-info/SOURCES.txt +3 -2
  19. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools.egg-info/requires.txt +5 -4
  20. package_dev_tools-0.8.4/src/package_dev_tools/pre_commit/check_coverage.py +0 -54
  21. package_dev_tools-0.8.4/src/package_dev_tools/pre_commit/check_shields.py +0 -25
  22. package_dev_tools-0.8.4/src/package_dev_tools/utils/package.py +0 -89
  23. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/LICENSE +0 -0
  24. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/bin/pre-commit/bump-version +0 -0
  25. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/bin/pre-commit/check-version +0 -0
  26. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/setup.cfg +0 -0
  27. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/__init__.py +0 -0
  28. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/__init__.py +0 -0
  29. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/instantiate_new_project/__init__.py +0 -0
  30. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/instantiate_new_project/cleanup_readme.py +0 -0
  31. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/instantiate_new_project/cleanup_workflows.py +0 -0
  32. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/instantiate_new_project/substitute_template_name/__init__.py +0 -0
  33. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/instantiate_new_project/substitute_template_name/project.py +0 -0
  34. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/template_sync/__init__.py +0 -0
  35. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/template_sync/git.py +0 -0
  36. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/actions/template_sync/trigger.py +0 -0
  37. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/__init__.py +0 -0
  38. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/check_coverage.py +0 -0
  39. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/check_shields.py +0 -0
  40. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/cleanup_readme.py +0 -0
  41. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/export_pre_commit_config.py +0 -0
  42. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/instantiate_new_project.py +0 -0
  43. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/substitute_template_name.py +0 -0
  44. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/sync_template.py +0 -0
  45. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/cli/trigger_template_sync.py +0 -0
  46. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/models/__init__.py +0 -0
  47. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/pre_commit/__init__.py +0 -0
  48. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/pre_commit/expand_docstrings.py +0 -0
  49. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/pre_commit/export_config.py +0 -0
  50. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/py.typed +0 -0
  51. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools/utils/__init__.py +0 -0
  52. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools.egg-info/dependency_links.txt +0 -0
  53. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools.egg-info/entry_points.txt +0 -0
  54. {package_dev_tools-0.8.4 → package_dev_tools-0.8.6}/src/package_dev_tools.egg-info/top_level.txt +0 -0
@@ -1,40 +1,41 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: package-dev-tools
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: CI/CD tools for Python packages
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
6
  License-Expression: MIT
7
7
  Project-URL: Source Code, https://github.com/quintenroets/package-dev-tools
8
- Requires-Python: >=3.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: package-utils<1,>=0.8.4
12
+ Requires-Dist: packaging<27,>=23.2
12
13
  Requires-Dist: powercli<1,>=0.4.0
13
14
  Requires-Dist: pygithub<3,>=2.1.1
14
15
  Requires-Dist: python-slugify<9,>=8.0.1
16
+ Requires-Dist: PyYaml<7,>=6.0.1
17
+ Requires-Dist: requests<3,>=2.31.0
15
18
  Requires-Dist: simple-classproperty<5,>=4.0.2
16
19
  Requires-Dist: superpathlib<3,>=2.0.12
17
- Requires-Dist: toml<1,>=0.10.2
18
20
  Requires-Dist: autotyping<25,>=23.3.0
19
21
  Requires-Dist: bump2version<2,>=1.0.1
20
22
  Requires-Dist: coverage<8,>=7.4.0
21
23
  Requires-Dist: libcst<2,>=1.7.0
22
24
  Requires-Dist: pre-commit<5,>=3.5.0
23
25
  Requires-Dist: pytest<10,>=9.0.2
24
- Requires-Dist: mypy<2.2,>=2.1
25
- Requires-Dist: ruff<0.16,>=0.15.6
26
+ Requires-Dist: mypy<2.4,>=2.3
27
+ Requires-Dist: ruff<0.17,>=0.16.3
26
28
  Provides-Extra: dev
27
29
  Requires-Dist: hypothesis<7,>=6.91.0; extra == "dev"
28
30
  Requires-Dist: package-dev-utils<1,>=0.1.6; extra == "dev"
29
31
  Requires-Dist: package-utils[context]<1,>=0.8.4; extra == "dev"
30
32
  Requires-Dist: types-requests<3,>=2.31.0.10; extra == "dev"
31
- Requires-Dist: types-toml<1,>=0.10.8.7; extra == "dev"
32
33
  Dynamic: license-file
33
34
 
34
35
  # Package Dev Tools
35
36
  [![PyPI version](https://badge.fury.io/py/package-dev-tools.svg)](https://badge.fury.io/py/package-dev-tools)
36
37
  ![PyPI downloads](https://img.shields.io/pypi/dm/package-dev-tools)
37
- ![Python version](https://img.shields.io/badge/python-3.10+-brightgreen)
38
+ ![Python version](https://img.shields.io/badge/python-3.11+-brightgreen)
38
39
  ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS%20%7c%20windows-brightgreen)
39
40
  ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
40
41
 
@@ -1,7 +1,7 @@
1
1
  # Package Dev Tools
2
2
  [![PyPI version](https://badge.fury.io/py/package-dev-tools.svg)](https://badge.fury.io/py/package-dev-tools)
3
3
  ![PyPI downloads](https://img.shields.io/pypi/dm/package-dev-tools)
4
- ![Python version](https://img.shields.io/badge/python-3.10+-brightgreen)
4
+ ![Python version](https://img.shields.io/badge/python-3.11+-brightgreen)
5
5
  ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS%20%7c%20windows-brightgreen)
6
6
  ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
7
7
 
@@ -1,19 +1,21 @@
1
1
  [project]
2
2
  name = "package-dev-tools"
3
- version = "0.8.4"
3
+ version = "0.8.6"
4
4
  description = "CI/CD tools for Python packages"
5
5
  authors = [{name = "Quinten Roets", email = "qdr2104@columbia.edu"}]
6
6
  license = "MIT"
7
7
  readme = "README.md"
8
- requires-python = ">=3.10"
8
+ requires-python = ">=3.11"
9
9
  dependencies = [
10
10
  "package-utils >=0.8.4, <1",
11
+ "packaging >=23.2, <27",
11
12
  "powercli >=0.4.0, <1",
12
13
  "pygithub >=2.1.1, <3",
13
14
  "python-slugify >=8.0.1, <9",
15
+ "PyYaml >=6.0.1, <7",
16
+ "requests >=2.31.0, <3",
14
17
  "simple-classproperty >=4.0.2, <5",
15
18
  "superpathlib >=2.0.12, <3",
16
- "toml >=0.10.2, <1",
17
19
 
18
20
  # pre-commit binaries
19
21
  "autotyping >=23.3.0, <25",
@@ -24,8 +26,8 @@ dependencies = [
24
26
  "pytest >=9.0.2, <10",
25
27
 
26
28
  # formatting binaries, stricter version range
27
- "mypy >= 2.1, < 2.2",
28
- "ruff >=0.15.6, <0.16"
29
+ "mypy >= 2.3, < 2.4",
30
+ "ruff >=0.16.3, <0.17"
29
31
  ]
30
32
 
31
33
  [project.optional-dependencies]
@@ -36,7 +38,6 @@ dev = [
36
38
 
37
39
  # mypy
38
40
  "types-requests >=2.31.0.10, <3",
39
- "types-toml >=0.10.8.7, <1",
40
41
  ]
41
42
 
42
43
  [project.urls]
@@ -63,6 +64,7 @@ build-backend = "setuptools.build_meta"
63
64
 
64
65
  [tool.coverage.run]
65
66
  command_line = "-m pytest tests"
67
+ relative_files = true
66
68
 
67
69
  [tool.coverage.report]
68
70
  precision = 4
@@ -84,6 +86,7 @@ fix = true
84
86
  select = ["ALL"]
85
87
  ignore = [
86
88
  "ANN401", # annotated with Any
89
+ "CPY001", # missing copyright notice
87
90
  "D1", # missing docstrings
88
91
  "D200", # one-line docstring
89
92
  "D203", # conflicts with D211
@@ -1,8 +1,9 @@
1
1
  from dataclasses import dataclass
2
2
 
3
+ from package_dev_tools.utils.git import GitInterface
4
+
3
5
  from .cleanup_readme import ReadmeCleaner
4
6
  from .cleanup_workflows import WorkflowsCleaner
5
- from .git import GitInterface
6
7
  from .substitute_template_name import NameSubstitutor
7
8
 
8
9
 
@@ -25,4 +26,4 @@ class ProjectInstantiator(NameSubstitutor):
25
26
  git = GitInterface(self.path)
26
27
  git.clean()
27
28
  if self.commit:
28
- git.commit()
29
+ git.commit("Instantiate new project")
@@ -2,11 +2,10 @@ import urllib.parse
2
2
  from collections.abc import Iterator
3
3
  from dataclasses import dataclass, field
4
4
 
5
- import cli
6
5
  from slugify import slugify
7
6
 
8
- from package_dev_tools.actions.instantiate_new_project.git import GitInterface
9
7
  from package_dev_tools.models import Path
8
+ from package_dev_tools.utils.git import GitInterface
10
9
  from package_dev_tools.utils.package import PackageInfo
11
10
 
12
11
  from .project import Project
@@ -71,7 +70,7 @@ class NameSubstitutor:
71
70
 
72
71
  def generate_paths_to_substitute(self) -> Iterator[Path]:
73
72
  workflows_folder = self.path / ".github" / "workflows"
74
- for path in self.generate_project_files():
73
+ for path in GitInterface(self.path).generate_files():
75
74
  # Modifying workflow files requires additional permissions.
76
75
  # Therefore, we don't do substitute those
77
76
  is_workflow = path.is_relative_to(workflows_folder)
@@ -87,9 +86,3 @@ class NameSubstitutor:
87
86
  )
88
87
  renamed_path = Path(renamed_path_str)
89
88
  path.rename(renamed_path)
90
-
91
- def generate_project_files(self) -> Iterator[Path]:
92
- command = ("git", "ls-tree", "-r", "HEAD", "--name-only")
93
- relative_paths = cli.capture_output_lines(command, cwd=self.path)
94
- for path in relative_paths:
95
- yield self.path / path
@@ -7,7 +7,7 @@ from superpathlib import Path
7
7
 
8
8
  from package_dev_tools import models
9
9
  from package_dev_tools.actions.instantiate_new_project import ProjectInstantiator
10
- from package_dev_tools.actions.instantiate_new_project.git import GitInterface
10
+ from package_dev_tools.utils.git import GitInterface
11
11
 
12
12
 
13
13
  @dataclass
@@ -52,7 +52,7 @@ class Merger: # pragma: nocover
52
52
  self.git.capture_output("checkout -B", name, "main")
53
53
  self.overwrite_project_files(path, self.template_directory)
54
54
  self.git.capture_output("add -A")
55
- self.git.commit()
55
+ self.git.commit("Instantiate new project", allow_empty=True)
56
56
 
57
57
  def overwrite_project_files(self, source: Path, destination: Path) -> None:
58
58
  self.remove_project_files(destination)
@@ -63,15 +63,9 @@ class Merger: # pragma: nocover
63
63
 
64
64
  def generate_project_files(self) -> Iterator[Path]:
65
65
  path = models.Path(self.repository_directory)
66
- for file in generate_project_files(path):
67
- yield file.relative_to(path)
66
+ return GitInterface(path).generate_relative_files()
68
67
 
69
68
  @classmethod
70
69
  def remove_project_files(cls, directory: Path) -> None:
71
- for file in generate_project_files(models.Path(directory)):
70
+ for file in GitInterface(models.Path(directory)).generate_files():
72
71
  file.unlink()
73
-
74
-
75
- def generate_project_files(path: models.Path) -> Iterator[models.Path]:
76
- instantiator = ProjectInstantiator(path=path, current_project_name="dummy")
77
- yield from instantiator.generate_project_files()
@@ -11,10 +11,7 @@ from slugify import slugify
11
11
  from superpathlib import Path
12
12
 
13
13
  from package_dev_tools import models
14
- from package_dev_tools.actions.instantiate_new_project.git import (
15
- GitInterface,
16
- resolve_git_binary,
17
- )
14
+ from package_dev_tools.utils.git import GitInterface, resolve_git_binary
18
15
 
19
16
  from . import git
20
17
  from .merge import Merger
@@ -79,7 +76,7 @@ class TemplateSyncer(git.Client):
79
76
  self.apply_ignore_patterns()
80
77
  path = models.Path(self.downloaded_repository_directory)
81
78
  try:
82
- GitInterface(path=path, commit_message=self.commit_message).commit()
79
+ GitInterface(path=path).commit(self.commit_message)
83
80
  is_updated = True
84
81
  except cli.CalledProcessError:
85
82
  is_updated = False
@@ -11,11 +11,11 @@ def extract_package_name(info: PackageInfo) -> None:
11
11
 
12
12
 
13
13
  def extract_required_python_version(info: PackageInfo) -> None:
14
- cli.console.print(info.required_python_version)
14
+ cli.console.print(info.python_versions.required)
15
15
 
16
16
 
17
17
  def extract_supported_python_versions(info: PackageInfo) -> None:
18
- cli.console.print(json.dumps(list(info.supported_python_versions)))
18
+ cli.console.print(json.dumps(info.python_versions.supported))
19
19
 
20
20
 
21
21
  package_name = create_entry_point(extract_package_name)
@@ -1,10 +1,7 @@
1
- from typing import TypeVar
1
+ from typing import Self
2
2
 
3
3
  import superpathlib
4
4
  from simple_classproperty import classproperty
5
- from typing_extensions import Self
6
-
7
- T = TypeVar("T", bound="Path")
8
5
 
9
6
 
10
7
  class Path(superpathlib.Path):
@@ -0,0 +1,55 @@
1
+ import sys
2
+ from io import StringIO
3
+
4
+ import cli
5
+ from coverage import Coverage
6
+ from coverage.results import should_fail_under
7
+
8
+ from package_dev_tools.models import Path
9
+ from package_dev_tools.utils.badge import Badge, BadgeUpdater
10
+ from package_dev_tools.utils.git import GitInterface
11
+
12
+
13
+ def check_coverage() -> None:
14
+ coverage = load_coverage()
15
+ files = [str(path) for path in GitInterface().generate_relative_files("*.py")]
16
+ percentage = coverage.report(files, ignore_errors=True, file=StringIO())
17
+ verify_fail_under(coverage, files, percentage)
18
+ has_changed = update_badge(coverage, files, percentage)
19
+ sys.exit(1 if has_changed else 0)
20
+
21
+
22
+ def load_coverage() -> Coverage:
23
+ coverage = Coverage()
24
+ if not Path(coverage.config.data_file).exists():
25
+ message = "No coverage results found."
26
+ raise FileNotFoundError(message)
27
+ coverage.load()
28
+ config = coverage.config
29
+ config.report_omit = [*config.run_omit, *(config.report_omit or [])]
30
+ return coverage
31
+
32
+
33
+ def verify_fail_under(coverage: Coverage, files: list[str], percentage: float) -> None:
34
+ config = coverage.config
35
+ if should_fail_under(percentage, config.fail_under, config.precision):
36
+ coverage.html_report(files, ignore_errors=True)
37
+ coverage.report(files, show_missing=True, ignore_errors=True)
38
+ current = format_percentage(percentage, config.precision)
39
+ required = format_percentage(config.fail_under, config.precision)
40
+ message = f"Coverage {current} is below the required {required}"
41
+ raise RuntimeError(message)
42
+
43
+
44
+ def update_badge(coverage: Coverage, files: list[str], percentage: float) -> bool:
45
+ badge = Badge("Coverage", f"coverage-{round(percentage)}%25")
46
+ has_changed = BadgeUpdater(badge).run()
47
+ if has_changed:
48
+ formatted_percentage = format_percentage(percentage, coverage.config.precision)
49
+ cli.console.print(f"Updated test coverage: {formatted_percentage}")
50
+ coverage.html_report(files, ignore_errors=True)
51
+ return has_changed
52
+
53
+
54
+ def format_percentage(value: float, precision: int) -> str:
55
+ return f"{value:.{precision}f}%"
@@ -1,12 +1,11 @@
1
1
  import importlib
2
2
 
3
- from package_utils.cli import create_entry_point, instantiate_from_cli_args
3
+ from package_utils.cli import create_entry_point
4
4
 
5
5
  from package_dev_tools.utils.package import PackageInfo
6
6
 
7
7
 
8
- def check_import() -> None:
9
- package_info = instantiate_from_cli_args(PackageInfo)
8
+ def check_import(package_info: PackageInfo) -> None:
10
9
  importlib.import_module(package_info.package_name)
11
10
 
12
11
 
@@ -0,0 +1,25 @@
1
+ from package_dev_tools.utils.badge import Badge, BadgeUpdater
2
+ from package_dev_tools.utils.package import PackageInfo
3
+ from package_dev_tools.utils.python_versions import PythonVersions
4
+
5
+
6
+ def check_shields() -> None:
7
+ info = PackageInfo()
8
+ python_versions = describe_python_versions(info.python_versions)
9
+ operating_systems = Badge.separator.join(info.supported_operating_systems)
10
+ badges = (
11
+ Badge("Python version", f"python-{python_versions}"),
12
+ Badge("Operating system", f"os-{operating_systems}"),
13
+ )
14
+ for badge in badges:
15
+ BadgeUpdater(badge).run()
16
+
17
+
18
+ def describe_python_versions(versions: PythonVersions) -> str:
19
+ return (
20
+ Badge.separator.join(versions.supported)
21
+ if versions.has_excluded_minors or versions.maximum == versions.required
22
+ else f"{versions.required}+"
23
+ if versions.maximum is None
24
+ else f"{versions.required}--{versions.maximum}"
25
+ )
@@ -10,6 +10,7 @@ class Badge:
10
10
  title: str
11
11
  content: str
12
12
  base_url: ClassVar[str] = "https://img.shields.io/badge"
13
+ separator: ClassVar[str] = "%20%7c%20"
13
14
  color: str = "brightgreen"
14
15
 
15
16
  @cached_property
@@ -2,6 +2,7 @@ import os
2
2
  import shlex
3
3
  import shutil
4
4
  import subprocess
5
+ from collections.abc import Iterator
5
6
  from dataclasses import dataclass, field
6
7
  from functools import cache
7
8
  from typing import Any
@@ -28,15 +29,23 @@ class GitInterface:
28
29
  path: Path = field(default_factory=Path.cwd)
29
30
  git_name: str = "Quinten"
30
31
  git_email: str = "quinten.roets@gmail.com"
31
- commit_message: str = "Instantiate new project"
32
32
 
33
33
  def clean(self) -> None:
34
34
  self.capture_output("add -A")
35
35
  self.capture_output("clean -fd")
36
36
 
37
- def commit(self) -> None:
37
+ def commit(self, message: str, *, allow_empty: bool = False) -> None:
38
38
  self.configure()
39
- self.capture_output("commit --no-verify -m", self.commit_message)
39
+ options = "--no-verify --allow-empty" if allow_empty else "--no-verify"
40
+ self.capture_output(f"commit {options} -m", message)
41
+
42
+ def generate_files(self, *patterns: str) -> Iterator[Path]:
43
+ return (self.path / path for path in self.generate_relative_files(*patterns))
44
+
45
+ def generate_relative_files(self, *patterns: str) -> Iterator[Path]:
46
+ command = "ls-files --cached --others --exclude-standard"
47
+ output = self.capture_output(command, *patterns)
48
+ return (Path(relative_path) for relative_path in output.splitlines())
40
49
 
41
50
  def capture_output(self, *args: CommandItem, **kwargs: Any) -> str:
42
51
  return self.create_runner(*args, **kwargs).capture_output()
@@ -0,0 +1,50 @@
1
+ import tomllib
2
+ import typing
3
+ from collections.abc import Iterator
4
+ from dataclasses import dataclass, field
5
+ from functools import cached_property
6
+ from typing import Any, ClassVar
7
+
8
+ from superpathlib import Path
9
+
10
+ from .python_versions import PythonVersions
11
+
12
+
13
+ @dataclass
14
+ class PackageInfo:
15
+ path: Path = field(default_factory=Path.cwd)
16
+ os_mapper: ClassVar[dict[str, str]] = {"ubuntu": "linux", "macos": "macOS"}
17
+
18
+ @property
19
+ def package_name(self) -> str:
20
+ package_data = self.pyproject_info["tool"]["setuptools"]["package-data"]
21
+ project_name = next(iter(package_data))
22
+ return typing.cast("str", project_name)
23
+
24
+ @property
25
+ def package_slug(self) -> str:
26
+ package_slug = self.pyproject_info["project"]["name"]
27
+ return typing.cast("str", package_slug)
28
+
29
+ @cached_property
30
+ def python_versions(self) -> PythonVersions:
31
+ requirement = self.pyproject_info["project"]["requires-python"]
32
+ return PythonVersions(typing.cast("str", requirement))
33
+
34
+ @cached_property
35
+ def pyproject_info(self) -> dict[str, Any]:
36
+ info_path = self.path / "pyproject.toml"
37
+ return tomllib.loads(info_path.text)
38
+
39
+ @property
40
+ def supported_operating_systems(self) -> Iterator[str]:
41
+ workflow_path = self.path / ".github" / "workflows" / "build.yml"
42
+ workflow = typing.cast("dict[str, Any]", workflow_path.yaml)
43
+ matrices = (
44
+ job.get("strategy", {}).get("matrix", {})
45
+ for job in workflow["jobs"].values()
46
+ )
47
+ entries = next(matrix["os"] for matrix in matrices if "os" in matrix)
48
+ for entry in typing.cast("list[str]", entries):
49
+ operating_system = entry.removesuffix("-latest")
50
+ yield self.os_mapper.get(operating_system, operating_system)
@@ -0,0 +1,77 @@
1
+ from collections.abc import Iterator
2
+ from dataclasses import dataclass
3
+ from functools import cache, cached_property
4
+ from typing import Any, ClassVar
5
+
6
+ import requests
7
+ from packaging.specifiers import SpecifierSet
8
+ from packaging.version import Version
9
+
10
+
11
+ @dataclass
12
+ class PythonVersions:
13
+ requirement: str
14
+ major: ClassVar[int] = 3
15
+
16
+ @property
17
+ def required(self) -> str:
18
+ return str(min(self.specified_versions))
19
+
20
+ @property
21
+ def supported(self) -> list[str]:
22
+ versions = (
23
+ self.specified_versions
24
+ if self.is_bounded
25
+ else self.included_versions(fetch_latest_python_version(self.major))
26
+ )
27
+ return [str(version) for version in versions]
28
+
29
+ @property
30
+ def maximum(self) -> str | None:
31
+ return str(max(self.specified_versions)) if self.is_bounded else None
32
+
33
+ @property
34
+ def has_excluded_minors(self) -> bool:
35
+ minors = [version.minor for version in self.specified_versions]
36
+ return minors != list(range(min(minors), max(minors) + 1))
37
+
38
+ @cached_property
39
+ def specified_versions(self) -> list[Version]:
40
+ return list(self.included_versions(self.search_ceiling))
41
+
42
+ @cached_property
43
+ def is_bounded(self) -> bool:
44
+ return self.search_ceiling not in self.specifiers
45
+
46
+ @cached_property
47
+ def search_ceiling(self) -> Version:
48
+ versions = (
49
+ Version(specifier.version.removesuffix(".*"))
50
+ for specifier in self.specifiers
51
+ )
52
+ minors = (version.minor for version in versions if version.major == self.major)
53
+ return self.version(max(minors, default=0) + 1)
54
+
55
+ def included_versions(self, ceiling: Version) -> Iterator[Version]:
56
+ candidates = (self.version(minor) for minor in range(ceiling.minor + 1))
57
+ return (version for version in candidates if version in self.specifiers)
58
+
59
+ @cached_property
60
+ def specifiers(self) -> SpecifierSet:
61
+ return SpecifierSet(self.requirement)
62
+
63
+ def version(self, minor: int) -> Version:
64
+ return Version(f"{self.major}.{minor}")
65
+
66
+
67
+ @cache
68
+ def fetch_latest_python_version(major: int) -> Version:
69
+ url = "https://www.python.org/api/v2/downloads/release/"
70
+ parameters = {"is_published": "true", "pre_release": "false"}
71
+ response = requests.get(url, params=parameters, timeout=10)
72
+ releases: list[dict[str, Any]] = response.json()
73
+ return max(
74
+ Version(release["name"].removeprefix("Python "))
75
+ for release in releases
76
+ if release["version"] == major
77
+ )
@@ -1,40 +1,41 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: package-dev-tools
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: CI/CD tools for Python packages
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
6
  License-Expression: MIT
7
7
  Project-URL: Source Code, https://github.com/quintenroets/package-dev-tools
8
- Requires-Python: >=3.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: package-utils<1,>=0.8.4
12
+ Requires-Dist: packaging<27,>=23.2
12
13
  Requires-Dist: powercli<1,>=0.4.0
13
14
  Requires-Dist: pygithub<3,>=2.1.1
14
15
  Requires-Dist: python-slugify<9,>=8.0.1
16
+ Requires-Dist: PyYaml<7,>=6.0.1
17
+ Requires-Dist: requests<3,>=2.31.0
15
18
  Requires-Dist: simple-classproperty<5,>=4.0.2
16
19
  Requires-Dist: superpathlib<3,>=2.0.12
17
- Requires-Dist: toml<1,>=0.10.2
18
20
  Requires-Dist: autotyping<25,>=23.3.0
19
21
  Requires-Dist: bump2version<2,>=1.0.1
20
22
  Requires-Dist: coverage<8,>=7.4.0
21
23
  Requires-Dist: libcst<2,>=1.7.0
22
24
  Requires-Dist: pre-commit<5,>=3.5.0
23
25
  Requires-Dist: pytest<10,>=9.0.2
24
- Requires-Dist: mypy<2.2,>=2.1
25
- Requires-Dist: ruff<0.16,>=0.15.6
26
+ Requires-Dist: mypy<2.4,>=2.3
27
+ Requires-Dist: ruff<0.17,>=0.16.3
26
28
  Provides-Extra: dev
27
29
  Requires-Dist: hypothesis<7,>=6.91.0; extra == "dev"
28
30
  Requires-Dist: package-dev-utils<1,>=0.1.6; extra == "dev"
29
31
  Requires-Dist: package-utils[context]<1,>=0.8.4; extra == "dev"
30
32
  Requires-Dist: types-requests<3,>=2.31.0.10; extra == "dev"
31
- Requires-Dist: types-toml<1,>=0.10.8.7; extra == "dev"
32
33
  Dynamic: license-file
33
34
 
34
35
  # Package Dev Tools
35
36
  [![PyPI version](https://badge.fury.io/py/package-dev-tools.svg)](https://badge.fury.io/py/package-dev-tools)
36
37
  ![PyPI downloads](https://img.shields.io/pypi/dm/package-dev-tools)
37
- ![Python version](https://img.shields.io/badge/python-3.10+-brightgreen)
38
+ ![Python version](https://img.shields.io/badge/python-3.11+-brightgreen)
38
39
  ![Operating system](https://img.shields.io/badge/os-linux%20%7c%20macOS%20%7c%20windows-brightgreen)
39
40
  ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
40
41
 
@@ -15,7 +15,6 @@ src/package_dev_tools/actions/__init__.py
15
15
  src/package_dev_tools/actions/instantiate_new_project/__init__.py
16
16
  src/package_dev_tools/actions/instantiate_new_project/cleanup_readme.py
17
17
  src/package_dev_tools/actions/instantiate_new_project/cleanup_workflows.py
18
- src/package_dev_tools/actions/instantiate_new_project/git.py
19
18
  src/package_dev_tools/actions/instantiate_new_project/instantiate_new_project.py
20
19
  src/package_dev_tools/actions/instantiate_new_project/substitute_template_name/__init__.py
21
20
  src/package_dev_tools/actions/instantiate_new_project/substitute_template_name/project.py
@@ -45,4 +44,6 @@ src/package_dev_tools/pre_commit/expand_docstrings.py
45
44
  src/package_dev_tools/pre_commit/export_config.py
46
45
  src/package_dev_tools/utils/__init__.py
47
46
  src/package_dev_tools/utils/badge.py
48
- src/package_dev_tools/utils/package.py
47
+ src/package_dev_tools/utils/git.py
48
+ src/package_dev_tools/utils/package.py
49
+ src/package_dev_tools/utils/python_versions.py
@@ -1,22 +1,23 @@
1
1
  package-utils<1,>=0.8.4
2
+ packaging<27,>=23.2
2
3
  powercli<1,>=0.4.0
3
4
  pygithub<3,>=2.1.1
4
5
  python-slugify<9,>=8.0.1
6
+ PyYaml<7,>=6.0.1
7
+ requests<3,>=2.31.0
5
8
  simple-classproperty<5,>=4.0.2
6
9
  superpathlib<3,>=2.0.12
7
- toml<1,>=0.10.2
8
10
  autotyping<25,>=23.3.0
9
11
  bump2version<2,>=1.0.1
10
12
  coverage<8,>=7.4.0
11
13
  libcst<2,>=1.7.0
12
14
  pre-commit<5,>=3.5.0
13
15
  pytest<10,>=9.0.2
14
- mypy<2.2,>=2.1
15
- ruff<0.16,>=0.15.6
16
+ mypy<2.4,>=2.3
17
+ ruff<0.17,>=0.16.3
16
18
 
17
19
  [dev]
18
20
  hypothesis<7,>=6.91.0
19
21
  package-dev-utils<1,>=0.1.6
20
22
  package-utils[context]<1,>=0.8.4
21
23
  types-requests<3,>=2.31.0.10
22
- types-toml<1,>=0.10.8.7
@@ -1,54 +0,0 @@
1
- import sys
2
-
3
- import cli
4
-
5
- from package_dev_tools.models import Path
6
- from package_dev_tools.utils.badge import Badge, BadgeUpdater
7
-
8
-
9
- def check_coverage(*, verify_all_files_tested: bool = True) -> None:
10
- verify_coverage_results()
11
- if verify_all_files_tested:
12
- verify_all_python_files_tested()
13
-
14
- try:
15
- coverage_percentage = cli.capture_output("coverage report -i --format total")
16
- except cli.CalledProcessError:
17
- cli.capture_output("coverage html -i", check=False)
18
- cli.run("coverage report -mi", check=False)
19
- raise
20
- coverage_percentage_has_changed = update_coverage_shield(coverage_percentage)
21
- if coverage_percentage_has_changed:
22
- cli.console.print(f"Updated test coverage: {coverage_percentage}%")
23
- cli.capture_output("coverage html")
24
- exit_code = 1 if coverage_percentage_has_changed else 0
25
- sys.exit(exit_code)
26
-
27
-
28
- def update_coverage_shield(coverage_percentage: float | str) -> bool:
29
- if isinstance(coverage_percentage, str):
30
- coverage_percentage = float(coverage_percentage)
31
- coverage_percentage_int = round(coverage_percentage)
32
- badge = Badge("Coverage", f"coverage-{coverage_percentage_int}%25")
33
- return BadgeUpdater(badge).run()
34
-
35
-
36
- def verify_all_python_files_tested() -> None:
37
- python_files = set(cli.capture_output_lines("git ls-files '*.py'"))
38
- coverage_lines = cli.capture_output_lines("coverage report -i", check=False)
39
- covered_files = {line.split()[0] for line in coverage_lines[2:-2]}
40
- not_covered_files = python_files - covered_files
41
- if not_covered_files:
42
- cli.run("coverage html -i", check=False)
43
- message_parts = (
44
- "The following files are not covered by tests:",
45
- *not_covered_files,
46
- )
47
- message = "\n\t".join(message_parts)
48
- raise RuntimeError(message)
49
-
50
-
51
- def verify_coverage_results() -> None:
52
- if not Path(".coverage").exists():
53
- message = "No coverage results found."
54
- raise OSError(message)
@@ -1,25 +0,0 @@
1
- from package_dev_tools.utils.badge import Badge, BadgeUpdater
2
- from package_dev_tools.utils.package import PackageInfo
3
-
4
-
5
- def check_shields() -> None:
6
- package_info = PackageInfo()
7
- python_version = create_python_version_badge(package_info=package_info)
8
- operating_systems = package_info.supported_operating_systems
9
- operating_system = "os-" + "%20%7c%20".join(operating_systems)
10
- badges = (
11
- Badge("Python version", python_version),
12
- Badge("Operating system", operating_system),
13
- )
14
- for badge in badges:
15
- BadgeUpdater(badge).run()
16
-
17
-
18
- def create_python_version_badge(package_info: PackageInfo) -> str:
19
- minimum_version = package_info.required_python_version
20
- if "," in package_info.listed_version: # pragma: nocover
21
- maximum_version = f"3.{package_info.latest_supported_python_minor}"
22
- version = minimum_version + "--" + maximum_version
23
- else:
24
- version = minimum_version + "+"
25
- return f"python-{version}"
@@ -1,89 +0,0 @@
1
- import typing
2
- from collections.abc import Iterator
3
- from dataclasses import dataclass, field
4
- from functools import cached_property
5
- from typing import Any, ClassVar
6
-
7
- import requests
8
- import toml
9
- from superpathlib import Path
10
-
11
- SUCCESS_CODE = 200
12
-
13
-
14
- @dataclass
15
- class PackageInfo:
16
- path: Path = field(default_factory=Path.cwd)
17
- os_mapper: ClassVar[dict[str, str]] = {"ubuntu": "linux", "macos": "macOS"}
18
-
19
- @property
20
- def package_name(self) -> str:
21
- package_data = self.pyproject_info["tool"]["setuptools"]["package-data"]
22
- project_name = next(iter(package_data))
23
- return typing.cast("str", project_name)
24
-
25
- @property
26
- def package_slug(self) -> str:
27
- package_slug = self.pyproject_info["project"]["name"]
28
- return typing.cast("str", package_slug)
29
-
30
- @cached_property
31
- def listed_version(self) -> str:
32
- version = self.pyproject_info["project"]["requires-python"].split(">=")[1]
33
- return typing.cast("str", version)
34
-
35
- @property
36
- def required_python_version(self) -> str:
37
- version = self.listed_version
38
- if "," in version:
39
- version = version.split(",")[0]
40
- return version
41
-
42
- @property
43
- def required_python_minor(self) -> int:
44
- minor_version = self.required_python_version.split(".")[-1]
45
- return int(minor_version)
46
-
47
- @property
48
- def supported_python_versions(self) -> Iterator[str]:
49
- latest_python_minor = self.latest_supported_python_minor
50
- minors = range(self.required_python_minor, latest_python_minor + 1)
51
- return (f"3.{minor_version}" for minor_version in minors)
52
-
53
- @property
54
- def latest_supported_python_minor(self) -> int:
55
- return (
56
- int(self.listed_version.split("<")[1].split(".")[-1]) - 1
57
- if "," in self.listed_version
58
- else self.retrieve_latest_python_minor()
59
- )
60
-
61
- def retrieve_latest_python_minor(self) -> int:
62
- minor_version = self.required_python_minor
63
- while self.release_exists(minor_version + 1):
64
- minor_version += 1
65
- return minor_version
66
-
67
- @classmethod
68
- def release_exists(cls, minor_version: int) -> bool:
69
- version = f"3.{minor_version}.0"
70
- base_url = "https://www.python.org/ftp/python/"
71
- filename = f"Python-{version}.tar.xz"
72
- url = f"{base_url}/{version}/{filename}"
73
- return requests.head(url, timeout=10).status_code == SUCCESS_CODE
74
-
75
- @property
76
- def pyproject_info(self) -> dict[str, Any]:
77
- info_path = self.path / "pyproject.toml"
78
- return toml.loads(info_path.text)
79
-
80
- @property
81
- def supported_operating_systems(self) -> Iterator[str]:
82
- workflow_path = self.path / ".github" / "workflows" / "build.yml"
83
- os_keyword = "os: ["
84
- os_line = next(line for line in workflow_path.lines if os_keyword in line)
85
- os_entries = os_line.split(os_keyword)[1].split("]")[0].split(",")
86
- for entry in os_entries:
87
- parsed_entry = entry.strip().replace("-latest", "")
88
- mapped_entry = self.os_mapper.get(parsed_entry, parsed_entry)
89
- yield mapped_entry