package-dev-tools 0.1.1__py3-none-any.whl

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.
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ from ..models import Path
2
+
3
+
4
+ def cleanup_readme() -> None:
5
+ path = Path.readme
6
+ delimiter = "=" * 60 + "\n"
7
+ keyword = "## Usage"
8
+ text_parts = path.text.split(delimiter)
9
+ text_parts[0] = text_parts[0].split(keyword)[0]
10
+ path.text = keyword.join(text_parts)
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from dataclasses import dataclass
5
+
6
+ import cli
7
+ from plib import Path
8
+ from slugify import slugify
9
+
10
+ from package_dev_tools.utils.package import extract_package_name, extract_package_slug
11
+
12
+
13
+ @dataclass
14
+ class Project:
15
+ package_slug: str
16
+ path: Path = Path.cwd()
17
+
18
+ def __post_init__(self) -> None:
19
+ self.check_package_slug()
20
+ self.package_name = slugify(self.package_slug, separator="_")
21
+ self.name = slugify(self.package_slug, separator=" ").title()
22
+
23
+ def check_package_slug(self) -> None:
24
+ is_valid = self.package_slug == slugify(self.package_slug) and self.package_slug
25
+ if not is_valid:
26
+ self.raise_invalid_naming_exception()
27
+
28
+ def raise_invalid_naming_exception(self) -> None:
29
+ suggested_name = slugify(self.package_slug)
30
+ message = (
31
+ f"The project name '{self.package_slug}' is invalid.\n"
32
+ f"Suggested name: {suggested_name}"
33
+ )
34
+ raise ValueError(message)
35
+
36
+
37
+ @dataclass
38
+ class NameSubstitutor:
39
+ project_name: str
40
+ path: Path
41
+ current_project_name: str = ""
42
+ custom_template_package_name: str = "python-package-qtemplate"
43
+
44
+ def __post_init__(self) -> None:
45
+ self.new_project = Project(self.project_name, self.path)
46
+ if not self.current_project_name:
47
+ self.current_project_name = self.extract_current_project_name()
48
+ self.template_project = Project(self.current_project_name, self.path)
49
+ self.substitutions = {
50
+ self.custom_template_package_name: self.template_project.package_slug,
51
+ self.template_project.name: self.new_project.name,
52
+ self.template_project.package_slug: self.new_project.package_slug,
53
+ self.template_project.package_name: self.new_project.package_name,
54
+ }
55
+
56
+ def extract_current_project_name(self) -> str:
57
+ package_slug = extract_package_slug(self.path)
58
+ if package_slug == self.custom_template_package_name:
59
+ package_name = extract_package_name(self.path)
60
+ package_slug = slugify(package_name, separator="-")
61
+ return package_slug
62
+
63
+ def run(self) -> None:
64
+ for path in self.generate_paths_to_substitute():
65
+ self.apply_substitutions(path)
66
+
67
+ def apply_substitutions(self, path: Path) -> None:
68
+ content = path.text
69
+ if any(to_replace in content for to_replace in self.substitutions):
70
+ for original, replacement in self.substitutions.items():
71
+ content = content.replace(original, replacement)
72
+ path.text = content
73
+
74
+ def generate_paths_to_substitute(self) -> Iterator[Path]:
75
+ workflows_folder = self.path / ".github" / "workflows"
76
+ for path in self.generate_project_files():
77
+ # Modifying workflow files requires additional permissions.
78
+ # Therefore, we don't do substitute those
79
+ is_workflow = path.is_relative_to(workflows_folder)
80
+ is_file = path.is_file()
81
+ if not is_workflow and is_file:
82
+ try:
83
+ path.text
84
+ has_text = True
85
+ except UnicodeDecodeError:
86
+ has_text = False
87
+ if has_text:
88
+ yield path
89
+ self.rename(path)
90
+
91
+ def rename(self, path: Path) -> None:
92
+ if any(name == self.template_project.package_name for name in path.parts):
93
+ renamed_path_str = str(path).replace(
94
+ self.template_project.package_name,
95
+ self.new_project.package_name,
96
+ )
97
+ renamed_path = Path(renamed_path_str)
98
+ path.rename(renamed_path)
99
+
100
+ def generate_project_files(self) -> Iterator[Path]:
101
+ command = ("git", "ls-tree", "-r", "HEAD", "--name-only")
102
+ relative_paths = cli.lines(command, cwd=self.path)
103
+ for path in relative_paths:
104
+ yield self.path / path
105
+
106
+
107
+ def substitute_template_name(
108
+ project_name: str = "",
109
+ path_str: str = "",
110
+ current_project_name: str = "",
111
+ ) -> None:
112
+ # TODO: use create instance from cli args from package-utils
113
+ path = Path(path_str) if path_str else Path.cwd()
114
+ NameSubstitutor(project_name, path, current_project_name=current_project_name).run()
@@ -0,0 +1,36 @@
1
+ from concurrent.futures import ThreadPoolExecutor
2
+ from dataclasses import dataclass
3
+
4
+ import github.Auth
5
+ from github import Github, UnknownObjectException
6
+ from github.Repository import Repository
7
+
8
+
9
+ @dataclass
10
+ class TemplateSyncTriggerer:
11
+ token: str
12
+ workflow_name: str = "sync-template.yml"
13
+ max_workers: int = 10
14
+
15
+ def __post_init__(self) -> None:
16
+ auth = github.Auth.Token(self.token)
17
+ self.client = Github(auth=auth)
18
+
19
+ def run(self) -> None:
20
+ paginated_repos = self.client.get_user().get_repos(type="owner")
21
+ repos = list(paginated_repos)
22
+ with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
23
+ executor.map(self.trigger_if_possible, repos)
24
+
25
+ def trigger_if_possible(self, repo: Repository) -> None:
26
+ try:
27
+ workflow = repo.get_workflow(self.workflow_name)
28
+ except UnknownObjectException:
29
+ workflow = None
30
+
31
+ if workflow is not None:
32
+ workflow.create_dispatch("main")
33
+
34
+
35
+ def trigger_template_sync(token: str) -> None:
36
+ TemplateSyncTriggerer(token).run()
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from package_dev_tools.pre_commit.check_coverage import check_coverage
4
+
5
+
6
+ def entry_point() -> None:
7
+ typer.run(check_coverage)
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from package_dev_tools.actions.cleanup_readme import cleanup_readme
4
+
5
+
6
+ def entry_point() -> None:
7
+ typer.run(cleanup_readme)
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from package_dev_tools.actions.substitute_template_name import substitute_template_name
4
+
5
+
6
+ def entry_point() -> None:
7
+ typer.run(substitute_template_name)
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from package_dev_tools.actions.trigger_template_sync import trigger_template_sync
4
+
5
+
6
+ def entry_point() -> None:
7
+ typer.run(trigger_template_sync)
@@ -0,0 +1 @@
1
+ from .path import Path
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TypeVar
4
+
5
+ import plib
6
+ from simple_classproperty import classproperty
7
+
8
+ T = TypeVar("T", bound="Path")
9
+
10
+
11
+ class Path(plib.Path): # type: ignore # TODO: remove after superpathlib fix
12
+ @classproperty
13
+ def readme(cls: type[T]) -> T: # type: ignore
14
+ return cls("README.md")
File without changes
@@ -0,0 +1,85 @@
1
+ import sys
2
+ import typing
3
+ from collections.abc import Iterator
4
+
5
+ import cli
6
+
7
+ from package_dev_tools.utils.package import extract_package_slug
8
+
9
+ from ..models import Path
10
+
11
+
12
+ def check_coverage(verify_all_files_tested: bool = True) -> None:
13
+ generate_coverage_results()
14
+ if verify_all_files_tested:
15
+ verify_all_python_files_tested()
16
+
17
+ coverage_percentage = cli.get("coverage report --format total")
18
+ coverage_percentage = typing.cast(str, coverage_percentage)
19
+ coverage_percentage_has_changed = update_coverage_shield(coverage_percentage)
20
+ if coverage_percentage_has_changed:
21
+ print(f"Updated test coverage: {coverage_percentage}%")
22
+ cli.get("coverage html")
23
+ exit_code = 1 if coverage_percentage_has_changed else 0
24
+ sys.exit(exit_code)
25
+
26
+
27
+ def update_coverage_shield(coverage_percentage: float | str) -> bool:
28
+ if isinstance(coverage_percentage, str):
29
+ coverage_percentage = float(coverage_percentage)
30
+ coverage_percentage_int = round(coverage_percentage)
31
+ markdown_line_start = "![Coverage]("
32
+ badge_url_root = "https://img.shields.io/badge"
33
+ badge_url = f"{badge_url_root}/Coverage-{coverage_percentage_int }%25-brightgreen"
34
+ markdown_line = f"{markdown_line_start}{badge_url})"
35
+ current_markdown_lines = Path.readme.lines
36
+ no_badge = not any(markdown_line_start in line for line in current_markdown_lines)
37
+ if no_badge:
38
+ raise Exception("README has no coverage badge yet.")
39
+ converage_percentage_has_changed = markdown_line not in current_markdown_lines
40
+ lines = (
41
+ markdown_line if line.startswith(markdown_line_start) else line
42
+ for line in current_markdown_lines
43
+ )
44
+ lines_with_empty_lines = *lines, ""
45
+ Path.readme.text = "\n".join(lines_with_empty_lines)
46
+ return converage_percentage_has_changed
47
+
48
+
49
+ def verify_all_python_files_tested() -> None:
50
+ python_files = set(generate_python_files())
51
+ coverage_lines = cli.lines("coverage report")
52
+ covered_files = set(line.split()[0] for line in coverage_lines[2:-2])
53
+ not_covered_files = python_files - covered_files
54
+ if not_covered_files:
55
+ message_parts = (
56
+ "The following files are not covered by tests:",
57
+ *not_covered_files,
58
+ )
59
+ message = "\n\t".join(message_parts)
60
+ raise Exception(message)
61
+
62
+
63
+ def generate_python_files() -> Iterator[str]:
64
+ project_folder = Path.cwd()
65
+ python_files = project_folder.rglob("*.py")
66
+ for path in python_files:
67
+ relative_path = path.relative_to(project_folder)
68
+ if relative_path.parts[0] != "build":
69
+ yield str(relative_path)
70
+
71
+
72
+ def generate_coverage_results() -> None:
73
+ coverage_results_path = Path(".coverage")
74
+ package_slug = extract_package_slug()
75
+ try:
76
+ package_info = cli.get("pip show", package_slug)
77
+ except cli.CalledProcessError:
78
+ is_installed_non_editable = False
79
+ else:
80
+ is_installed_non_editable = "Editable project location: " not in package_info
81
+ if is_installed_non_editable:
82
+ cli.run("pip uninstall -y", package_slug)
83
+ coverage_results_path.unlink(missing_ok=True)
84
+ if not Path(".coverage").exists():
85
+ cli.run("coverage run")
@@ -0,0 +1 @@
1
+
File without changes
@@ -0,0 +1 @@
1
+ from .utils import extract_package_name, extract_package_slug
@@ -0,0 +1,23 @@
1
+ import tomllib
2
+ from typing import Any
3
+
4
+ from plib import Path
5
+
6
+
7
+ def extract_package_name(path: Path | None = None) -> str:
8
+ info = extract_pyproject_info(path)
9
+ package_data = info["tool"]["setuptools"]["package-data"]
10
+ project_name = next(iter(package_data))
11
+ return project_name
12
+
13
+
14
+ def extract_package_slug(path: Path | None = None) -> str:
15
+ info = extract_pyproject_info(path)
16
+ return info["project"]["name"]
17
+
18
+
19
+ def extract_pyproject_info(path: Path | None) -> dict[str, Any]:
20
+ if path is None:
21
+ path = Path.cwd()
22
+ info_path = path / "pyproject.toml"
23
+ return tomllib.loads(info_path.text)
@@ -0,0 +1 @@
1
+ from .utils import clear_cli_args, set_cli_args
@@ -0,0 +1,14 @@
1
+ import sys
2
+ from typing import Any
3
+
4
+ from _pytest.monkeypatch import MonkeyPatch
5
+
6
+
7
+ def clear_cli_args(monkeypatch: MonkeyPatch) -> None:
8
+ set_cli_args(monkeypatch)
9
+
10
+
11
+ def set_cli_args(monkeypatch: MonkeyPatch, *args: Any) -> None:
12
+ str_args = (str(arg) for arg in args)
13
+ sys_args = ["test_create_instance", *str_args]
14
+ monkeypatch.setattr(sys, "argv", sys_args)
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # don't bump/check bump in GitHub actions
4
+ if [ "$(git branch --show-current)" = "main" ]; then
5
+ if [ "$GITHUB_ACTIONS" != "true" ]; then
6
+ if ! git diff --staged pyproject.toml | grep -q version; then
7
+ bump2version --config-file .bumpversion.cfg patch
8
+ fi
9
+ fi
10
+ fi
@@ -0,0 +1,4 @@
1
+ #! /bin/env bash
2
+
3
+ rm workflows/instantiate-package-dev-tools.yml
4
+ rm workflows/trigger-template-sync.yml
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Quinten Roets
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.1
2
+ Name: package-dev-tools
3
+ Version: 0.1.1
4
+ Summary: Python package
5
+ Author-email: Quinten Roets <qdr2104@columbia.edu>
6
+ License: MIT
7
+ Project-URL: Source Code, https://github.com/quintenroets/package-dev-tools
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: pygithub >=2.1
12
+ Requires-Dist: python-slugify >=8.0
13
+ Requires-Dist: simple-classproperty >=4.0
14
+ Requires-Dist: superpathlib >=1.3
15
+ Requires-Dist: typer >=0.9
16
+ Requires-Dist: quinten-cli >=1.1
17
+ Provides-Extra: dev
18
+ Requires-Dist: build ; extra == 'dev'
19
+ Requires-Dist: bump2version ; extra == 'dev'
20
+ Requires-Dist: coverage ; extra == 'dev'
21
+ Requires-Dist: hypothesis ; extra == 'dev'
22
+ Requires-Dist: pre-commit ; extra == 'dev'
23
+ Requires-Dist: pytest ; extra == 'dev'
24
+ Requires-Dist: pytest-mypy-plugins ; extra == 'dev'
25
+ Requires-Dist: package-dev-tools ; extra == 'dev'
26
+
27
+ # Package Dev Tools
28
+ [![PyPI version](https://badge.fury.io/py/package-dev-tools.svg)](https://badge.fury.io/py/package-dev-tools)
29
+ ![Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen)
30
+ ## Usage
31
+ * Use [actions](https://github.com/quintenroets/package-dev-tools/tree/main/actions) in .github/workflows
32
+ * Use [pre-commit hooks](https://github.com/quintenroets/package-dev-tools/tree/main/.pre-commit-hooks.yaml) in .pre-commit-config.yaml
33
+ ## Installation
34
+ ```shell
35
+ pip install package-dev-tools
36
+ ```
37
+ make sure to use Python 3.10+
@@ -0,0 +1,29 @@
1
+ package_dev_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ package_dev_tools/py.typed,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
3
+ package_dev_tools/actions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ package_dev_tools/actions/cleanup_readme.py,sha256=zXQYuH2ozySGYG0zjzT-d-47njcYQCbC38Cqbkjf0FU,275
5
+ package_dev_tools/actions/substitute_template_name.py,sha256=v1dXBkgeKOPMOUkUnrgUlzkR9j85YXiJm_jQIWP48yI,4312
6
+ package_dev_tools/actions/trigger_template_sync.py,sha256=TWJvzc33sBBk26djwO3WgI3uopcHm3yIFHs0ziE1A4Y,1088
7
+ package_dev_tools/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ package_dev_tools/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ package_dev_tools/interfaces/cli/check_coverage.py,sha256=FEW2owlotguGQQ3dNSMRdV7W4L464kZPFIl8rbln_to,144
10
+ package_dev_tools/interfaces/cli/cleanup_readme.py,sha256=u4G9sOGbynNwKC-9TlNjMONiZEewxYfrluz8aORCFZc,141
11
+ package_dev_tools/interfaces/cli/substitute_template_name.py,sha256=r3b1ApRg1--Vvw0C0DpDUO7Emuj5lKmuOzO8jyO2URw,171
12
+ package_dev_tools/interfaces/cli/trigger_template_sync.py,sha256=m4qM9EcAVVOSlp0HiRIDL-uQp6N5wlJGlmSTOTyFtnE,162
13
+ package_dev_tools/models/__init__.py,sha256=1OeLZ6FZ5gAjlerAjc69Vx0AqWLSx6OSqNCpuzkCZQg,23
14
+ package_dev_tools/models/path.py,sha256=S5QYiFx18oYG0MYH75axKTtoH4_Fr72zna5GxQ9ljtw,336
15
+ package_dev_tools/pre_commit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ package_dev_tools/pre_commit/check_coverage.py,sha256=0HBFRGzeSYQiG47uv5hFymLXl8UIEdDKKFjkrpBLWYE,3198
17
+ package_dev_tools/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ package_dev_tools/utils/package/__init__.py,sha256=qDQe6KPiKE-5tUo57zVgTCIu3ej-4_LHmeDQWDMJGyc,62
19
+ package_dev_tools/utils/package/utils.py,sha256=iMWZs4EtjfAQAhFE1vmdGugl-S9AC9v5GkX8_phOZF8,623
20
+ package_dev_tools/utils/tests/__init__.py,sha256=2PBvBgS2em5pX-VCEhQ3LWJbQDiKl4a3FkpYi8nnp48,48
21
+ package_dev_tools/utils/tests/utils.py,sha256=LExKTxz-cMxOGjW7UmVK5VWYmYnc0ZXd5f4suI5X10U,371
22
+ package_dev_tools-0.1.1.data/scripts/check-version,sha256=Xwnoo_HQJAOqRlsL5CypDQQDa0iOm0MVDoezTLsUg0c,314
23
+ package_dev_tools-0.1.1.data/scripts/cleanup-workflows,sha256=Sn81o-jvTK-r1RR2q-J9DCA8W7NxcQsG_ddjLMLvLrg,104
24
+ package_dev_tools-0.1.1.dist-info/LICENSE,sha256=ENpNaBSvIV7ifD7iNz_-lBhImbiYowqPzBc5V5R8IhM,1070
25
+ package_dev_tools-0.1.1.dist-info/METADATA,sha256=EhD9KtSSDfHv-A3M2R6DAXpS_GCXXYjzYDCkeAQc-oc,1425
26
+ package_dev_tools-0.1.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
27
+ package_dev_tools-0.1.1.dist-info/entry_points.txt,sha256=FXlki7scg9b9vz0VKu3Xas23c0v_0daF8FoVZVA7rBU,353
28
+ package_dev_tools-0.1.1.dist-info/top_level.txt,sha256=PqhxIcDJGvTSOvYGWFahqZFabfDh2COOD1aRPS_7WOg,18
29
+ package_dev_tools-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ check-coverage = package_dev_tools.interfaces.cli.check_coverage:entry_point
3
+ cleanup-readme = package_dev_tools.interfaces.cli.cleanup_readme:entry_point
4
+ substitute-template-name = package_dev_tools.interfaces.cli.substitute_template_name:entry_point
5
+ trigger-template-sync = package_dev_tools.interfaces.cli.cleanup_readme:entry_point
@@ -0,0 +1 @@
1
+ package_dev_tools