Skip to content

Commit

Permalink
FEAT: set preferred Python version for dev environment (#245)
Browse files Browse the repository at this point in the history
* DX: run GitHub workflows on Python version
* DX: specify Python version in GitPod config
* DX: update Python version in Conda environment
* DX: update Python version in Read the Docs config
* DX: upgrade to `ubuntu-22.04` in Read the Docs config
* MAINT: use `Path.exists()` where possible
  • Loading branch information
redeboer authored Dec 6, 2023
1 parent 9eceaee commit 1cb1ef2
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 34 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"prereleased",
"prettierignore",
"prettierrc",
"pyenv",
"pyright",
"pyupgrade",
"redeboer",
Expand Down
1 change: 1 addition & 0 deletions .gitpod.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
tasks:
- init: pyenv local 3.8
- init: pip install -e .[dev]

github:
Expand Down
32 changes: 30 additions & 2 deletions src/repoma/check_dev_files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from __future__ import annotations

import re
import sys
from argparse import ArgumentParser
from typing import Sequence
from typing import TYPE_CHECKING, Any, Sequence

from repoma.check_dev_files.deprecated import remove_deprecated_tools
from repoma.utilities.executor import Executor
Expand All @@ -13,6 +14,7 @@
black,
citation,
commitlint,
conda,
cspell,
editorconfig,
github_labels,
Expand All @@ -26,6 +28,7 @@
pyright,
pytest,
pyupgrade,
readthedocs,
release_drafter,
ruff,
setup_cfg,
Expand All @@ -34,6 +37,9 @@
vscode,
)

if TYPE_CHECKING:
from repoma.utilities.project_info import PythonVersion


def main(argv: Sequence[str] | None = None) -> int:
parser = _create_argparse()
Expand All @@ -42,9 +48,11 @@ def main(argv: Sequence[str] | None = None) -> int:
if not args.repo_title:
args.repo_title = args.repo_name
has_notebooks = not args.no_notebooks
dev_python_version = __get_python_version(args.dev_python_version)
executor = Executor()
executor(citation.main)
executor(commitlint.main)
executor(conda.main, dev_python_version)
executor(cspell.main, args.no_cspell_update)
executor(editorconfig.main, args.no_python)
if not args.allow_labels:
Expand All @@ -54,6 +62,7 @@ def main(argv: Sequence[str] | None = None) -> int:
github_workflows.main,
allow_deprecated=args.allow_deprecated_workflows,
doc_apt_packages=_to_list(args.doc_apt_packages),
python_version=dev_python_version,
no_macos=args.no_macos,
no_pypi=args.no_pypi,
no_version_branches=args.no_version_branches,
Expand Down Expand Up @@ -82,9 +91,10 @@ def main(argv: Sequence[str] | None = None) -> int:
update_pip_constraints.main,
cron_frequency=args.pin_requirements,
)
executor(readthedocs.main, dev_python_version)
executor(remove_deprecated_tools, args.keep_issue_templates)
executor(vscode.main, has_notebooks)
executor(gitpod.main, args.no_gitpod)
executor(gitpod.main, args.no_gitpod, dev_python_version)
executor(precommit.main)
return executor.finalize(exception=False)

Expand Down Expand Up @@ -178,6 +188,13 @@ def _create_argparse() -> ArgumentParser:
default=False,
help="Do not perform the check on labels.toml",
)
parser.add_argument(
"--dev-python-version",
default="3.8",
help="Specify the Python version for your developer environment",
required=False,
type=str,
)
parser.add_argument(
"--no-macos",
action="store_true",
Expand Down Expand Up @@ -263,5 +280,16 @@ def _to_list(arg: str) -> list[str]:
return sorted(space_separated.split(" "))


def __get_python_version(arg: Any) -> PythonVersion:
if not isinstance(arg, str):
msg = f"--dev-python-version must be a string, not {type(arg).__name__}"
raise TypeError(msg)
arg = arg.strip()
if not re.match(r"^3\.\d+$", arg):
msg = f"Invalid Python version: {arg}"
raise ValueError(msg)
return arg # type: ignore[return-value]


if __name__ == "__main__":
sys.exit(main())
73 changes: 73 additions & 0 deletions src/repoma/check_dev_files/conda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Update the :file:`environment.yml` Conda environment file."""

from __future__ import annotations

from typing import TYPE_CHECKING

from ruamel.yaml.scalarstring import PlainScalarString

from repoma.errors import PrecommitError
from repoma.utilities import CONFIG_PATH
from repoma.utilities.project_info import PythonVersion, get_constraints_file
from repoma.utilities.yaml import create_prettier_round_trip_yaml

if TYPE_CHECKING:
from ruamel.yaml.comments import CommentedMap, CommentedSeq


def main(python_version: PythonVersion) -> None:
if not CONFIG_PATH.conda.exists():
return
yaml = create_prettier_round_trip_yaml()
conda_env: CommentedMap = yaml.load(CONFIG_PATH.conda)
conda_deps: CommentedSeq = conda_env.get("dependencies", [])

updated = _update_python_version(python_version, conda_deps)
updated |= _update_pip_dependencies(python_version, conda_deps)
if updated:
yaml.dump(conda_env, CONFIG_PATH.conda)
msg = f"Set the Python version in {CONFIG_PATH.conda} to {python_version}"
raise PrecommitError(msg)


def _update_python_version(version: PythonVersion, conda_deps: CommentedSeq) -> bool:
idx = __find_python_dependency_index(conda_deps)
expected = f"python=={version}.*"
if idx is not None and conda_deps[idx] != expected:
conda_deps[idx] = expected
return True
return False


def _update_pip_dependencies(version: PythonVersion, conda_deps: CommentedSeq) -> bool:
pip_deps = __get_pip_dependencies(conda_deps)
if pip_deps is None:
return False
constraints_file = get_constraints_file(version)
if constraints_file is None:
expected_pip = "-e .[dev]"
else:
expected_pip = f"-c {constraints_file} -e .[dev]"
if len(pip_deps) and pip_deps[0] != expected_pip:
pip_deps[0] = PlainScalarString(expected_pip)
return True
return False


def __find_python_dependency_index(dependencies: CommentedSeq) -> int | None:
for i, dep in enumerate(dependencies):
if not isinstance(dep, str):
continue
if dep.strip().startswith("python"):
return i
return None


def __get_pip_dependencies(dependencies: CommentedSeq) -> CommentedSeq | None:
for dep in dependencies:
if not isinstance(dep, dict):
continue
pip_deps = dep.get("pip")
if pip_deps is not None and isinstance(pip_deps, list):
return pip_deps
return None
31 changes: 22 additions & 9 deletions src/repoma/check_dev_files/github_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from repoma.utilities import CONFIG_PATH, REPOMA_DIR, hash_file, write
from repoma.utilities.executor import Executor
from repoma.utilities.precommit import PrecommitConfig
from repoma.utilities.project_info import get_pypi_name
from repoma.utilities.project_info import PythonVersion, get_pypi_name
from repoma.utilities.vscode import (
add_extension_recommendation,
remove_extension_recommendation,
Expand All @@ -34,6 +34,7 @@ def main(
no_macos: bool,
no_pypi: bool,
no_version_branches: bool,
python_version: PythonVersion,
single_threaded: bool,
skip_tests: list[str],
test_extras: list[str],
Expand All @@ -45,6 +46,7 @@ def main(
allow_deprecated,
doc_apt_packages,
no_macos,
python_version,
single_threaded,
skip_tests,
test_extras,
Expand All @@ -59,7 +61,7 @@ def update() -> None:
yaml = create_prettier_round_trip_yaml()
workflow_path = CONFIG_PATH.github_workflow_dir / "cd.yml"
expected_data = yaml.load(REPOMA_DIR / workflow_path)
if no_pypi or not os.path.exists(CONFIG_PATH.setup_cfg):
if no_pypi or not CONFIG_PATH.setup_cfg.exists():
del expected_data["jobs"]["pypi"]
if no_version_branches:
del expected_data["jobs"]["push"]
Expand Down Expand Up @@ -93,6 +95,7 @@ def _update_ci_workflow(
allow_deprecated: bool,
doc_apt_packages: list[str],
no_macos: bool,
python_version: PythonVersion,
single_threaded: bool,
skip_tests: list[str],
test_extras: list[str],
Expand All @@ -102,6 +105,7 @@ def update() -> None:
REPOMA_DIR / CONFIG_PATH.github_workflow_dir / "ci.yml",
doc_apt_packages,
no_macos,
python_version,
single_threaded,
skip_tests,
test_extras,
Expand Down Expand Up @@ -135,32 +139,41 @@ def _get_ci_workflow(
path: Path,
doc_apt_packages: list[str],
no_macos: bool,
python_version: PythonVersion,
single_threaded: bool,
skip_tests: list[str],
test_extras: list[str],
) -> tuple[YAML, dict]:
yaml = create_prettier_round_trip_yaml()
config = yaml.load(path)
__update_doc_section(config, doc_apt_packages)
__update_doc_section(config, doc_apt_packages, python_version)
__update_pytest_section(config, no_macos, single_threaded, skip_tests, test_extras)
__update_style_section(config)
__update_style_section(config, python_version)
return yaml, config


def __update_doc_section(config: CommentedMap, apt_packages: list[str]) -> None:
def __update_doc_section(
config: CommentedMap, apt_packages: list[str], python_version: PythonVersion
) -> None:
if not os.path.exists("docs/"):
del config["jobs"]["doc"]
else:
with_section = config["jobs"]["doc"]["with"]
if python_version != "3.8":
with_section["python-version"] = DoubleQuotedScalarString(python_version)
if apt_packages:
with_section["apt-packages"] = " ".join(apt_packages)
if not os.path.exists(CONFIG_PATH.readthedocs):
if not CONFIG_PATH.readthedocs.exists():
with_section["gh-pages"] = True
__update_with_section(config, job_name="doc")


def __update_style_section(config: CommentedMap) -> None:
if not os.path.exists(CONFIG_PATH.precommit):
def __update_style_section(config: CommentedMap, python_version: PythonVersion) -> None:
if python_version != "3.8":
config["jobs"]["style"]["with"] = {
"python-version": DoubleQuotedScalarString(python_version)
}
if not CONFIG_PATH.precommit.exists():
del config["jobs"]["style"]
else:
cfg = PrecommitConfig.load()
Expand All @@ -182,7 +195,7 @@ def __update_pytest_section(
with_section = config["jobs"]["pytest"]["with"]
if test_extras:
with_section["additional-extras"] = ",".join(test_extras)
if os.path.exists(CONFIG_PATH.codecov):
if CONFIG_PATH.codecov.exists():
with_section["coverage-target"] = __get_package_name()
if not no_macos:
with_section["macos-python-version"] = DoubleQuotedScalarString("3.9")
Expand Down
23 changes: 13 additions & 10 deletions src/repoma/check_dev_files/gitpod.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@

from repoma.errors import PrecommitError
from repoma.utilities import CONFIG_PATH, REPOMA_DIR
from repoma.utilities.project_info import get_repo_url
from repoma.utilities.project_info import (
PythonVersion,
get_constraints_file,
get_repo_url,
)
from repoma.utilities.readme import add_badge
from repoma.utilities.yaml import write_yaml

__CONSTRAINTS_FILE = ".constraints/py3.8.txt"


def main(no_gitpod: bool) -> None:
def main(no_gitpod: bool, python_version: PythonVersion) -> None:
if no_gitpod:
if CONFIG_PATH.gitpod.exists():
os.remove(CONFIG_PATH.gitpod)
msg = f"Removed {CONFIG_PATH.gitpod} as requested by --no-gitpod"
raise PrecommitError(msg)
return
pin_dependencies = os.path.exists(__CONSTRAINTS_FILE)
error_message = ""
expected_config = _generate_gitpod_config(pin_dependencies)
expected_config = _generate_gitpod_config(python_version)
if CONFIG_PATH.gitpod.exists():
with open(CONFIG_PATH.gitpod) as stream:
existing_config = yaml.load(stream, Loader=yaml.SafeLoader)
Expand Down Expand Up @@ -51,14 +52,16 @@ def _extract_extensions() -> dict:
return {}


def _generate_gitpod_config(pin_dependencies: bool) -> dict:
def _generate_gitpod_config(python_version: PythonVersion) -> dict:
with open(REPOMA_DIR / ".template" / CONFIG_PATH.gitpod) as stream:
gitpod_config = yaml.load(stream, Loader=yaml.SafeLoader)
tasks = gitpod_config["tasks"]
if pin_dependencies:
tasks[0]["init"] = f"pip install -c {__CONSTRAINTS_FILE} -e .[dev]"
tasks[0]["init"] = f"pyenv local {python_version}"
constraints_file = get_constraints_file(python_version)
if constraints_file is None:
tasks[1]["init"] = "pip install -e .[dev]"
else:
tasks[0]["init"] = "pip install -e .[dev]"
tasks[1]["init"] = f"pip install -c {constraints_file} -e .[dev]"
extensions = _extract_extensions()
if extensions:
gitpod_config["vscode"] = {"extensions": extensions}
Expand Down
Loading

0 comments on commit 1cb1ef2

Please sign in to comment.