Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/18 handle being offline #47

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/usethis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import typer
from rich.console import Console

console = Console()

offline_opt = typer.Option(False, "--offline", help="Disable network access")
Empty file removed src/usethis/_errors.py
Empty file.
2 changes: 1 addition & 1 deletion src/usethis/_github/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def get_github_latest_tag(owner: str, repo: str) -> str:
try:
response = requests.get(api_url, timeout=1)
response.raise_for_status() # Raise an error for HTTP issues
except requests.exceptions.HTTPError as err:
except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError) as err:
raise GitHubTagError(f"Failed to fetch tags from GitHub API: {err}")

tags = response.json()
Expand Down
14 changes: 2 additions & 12 deletions src/usethis/_pyproject/core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Literal, assert_never
from typing import Any

import mergedeep

Expand Down Expand Up @@ -97,8 +97,6 @@ def remove_config_value(id_keys: list[str], *, missing_ok: bool = False) -> None
def append_config_list(
id_keys: list[str],
values: list[Any],
*,
order: Literal["sorted", "preserved"] = "sorted",
) -> list[str]:
"""Append values to a list in the pyproject.toml configuration file."""
pyproject = read_pyproject_toml()
Expand All @@ -115,15 +113,7 @@ def append_config_list(
contents = {key: contents}
pyproject = mergedeep.merge(pyproject, contents)
else:
# Append to the existing configuration.
if order == "sorted":
new_values = sorted(p + values)
elif order == "preserved":
new_values = p + values
else:
assert_never(order)

p_parent[id_keys[-1]] = new_values
p_parent[id_keys[-1]] = p + values

write_pyproject_toml(pyproject)

Expand Down
18 changes: 12 additions & 6 deletions src/usethis/_pytest/core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import shutil
from pathlib import Path

from usethis import console


def add_pytest_dir():
def add_pytest_dir() -> None:
tests_dir = Path.cwd() / "tests"

if not tests_dir.exists():
Expand All @@ -20,14 +21,19 @@ def add_pytest_dir():
)


def remove_pytest_dir():
def remove_pytest_dir() -> None:
tests_dir = Path.cwd() / "tests"

if not tests_dir.exists():
# Early exit; tests directory does not exist
return

console.print(
"☐ Reconfigure the /tests directory to run without pytest", style="blue"
)
# Note we don't actually remove the directory, just explain what needs to be done.
if set(tests_dir.iterdir()) <= {tests_dir / "conftest.py"}:
# The only file in the directory is conftest.py
console.print("✔ Removing '/tests'.", style="green")
shutil.rmtree(tests_dir)
else:
console.print(
"☐ Reconfigure the /tests directory to run without pytest", style="blue"
)
# Note we don't actually remove the directory, just explain what needs to be done.
1 change: 0 additions & 1 deletion src/usethis/_ruff/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

def select_ruff_rules(rules: list[str]) -> None:
"""Add ruff rules to the project."""

rules = sorted(set(rules) - set(get_ruff_rules()))

if not rules:
Expand Down
11 changes: 11 additions & 0 deletions src/usethis/_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import socket
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
Expand All @@ -13,3 +14,13 @@ def change_cwd(new_dir: Path) -> Generator[None, None, None]:
yield
finally:
os.chdir(old_dir)


def is_offline() -> bool:
try:
# Connect to Google's DNS server
socket.create_connection(("8.8.8.8", 53), timeout=3)
except OSError:
return True
else:
return False
42 changes: 2 additions & 40 deletions src/usethis/_tool.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import re
import subprocess
from abc import abstractmethod
from pathlib import Path
from typing import Protocol
Expand All @@ -20,7 +18,7 @@
set_config_value,
)
from usethis._pyproject.io import read_pyproject_toml
from usethis._uv.deps import get_dev_deps
from usethis._uv.deps import is_dep_used


class Tool(Protocol):
Expand Down Expand Up @@ -63,7 +61,7 @@ def get_associated_ruff_rules(self) -> list[str]:

def is_used(self) -> bool:
"""Whether the tool is being used in the current project."""
return any(_strip_extras(dep) in get_dev_deps() for dep in self.dev_deps)
return any(is_dep_used(dep) for dep in self.dev_deps)

def add_pre_commit_repo_config(self) -> None:
"""Add the tool's pre-commit configuration."""
Expand Down Expand Up @@ -154,42 +152,6 @@ def remove_pyproject_configs(self) -> None:
)
first_removal = False

def add_dev_deps(self) -> None:
"""Add the tool's development dependencies, if not already added."""
existing_dev_deps = get_dev_deps()

for dep in self.dev_deps:
if _strip_extras(dep) in existing_dev_deps:
# Early exit; the tool is already a dev dependency.
continue

console.print(
f"✔ Adding '{dep}' as a development dependency.", style="green"
)
subprocess.run(["uv", "add", "--dev", "--quiet", dep], check=True)

def remove_dev_deps(self) -> None:
"""Remove the tool's development dependencies, if present."""
existing_dev_deps = get_dev_deps()

for dep in self.dev_deps:
if _strip_extras(dep) not in existing_dev_deps:
# Early exit; the tool is already not a dev dependency.
continue

console.print(
f"✔ Removing '{dep}' as a development dependency.",
style="green",
)
subprocess.run(
["uv", "remove", "--dev", "--quiet", _strip_extras(dep)], check=True
)


def _strip_extras(dep: str) -> str:
"""Remove extras from a dependency string."""
return re.sub(r"\[.*\]", "", dep)


class PreCommitTool(Tool):
@property
Expand Down
59 changes: 59 additions & 0 deletions src/usethis/_uv/deps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import re
import subprocess

from packaging.requirements import Requirement
from pydantic import TypeAdapter

from usethis import console
from usethis._pyproject.io import read_pyproject_toml


Expand All @@ -14,3 +18,58 @@ def get_dev_deps() -> list[str]:
req_strs = TypeAdapter(list[str]).validate_python(dev_deps_section)
reqs = [Requirement(req_str) for req_str in req_strs]
return [req.name for req in reqs]


def add_dev_deps(pypi_names: list[str], *, offline: bool) -> None:
"""Add a package as a development dependency, if not already added."""
existing_dev_deps = get_dev_deps()

for dep in pypi_names:
if _strip_extras(dep) in existing_dev_deps:
# Early exit; the tool is already a dev dependency.
continue

console.print(f"✔ Adding '{dep}' as a development dependency.", style="green")
if not offline:
subprocess.run(
["uv", "add", "--dev", "--quiet", dep],
check=True,
)
else:
subprocess.run(
["uv", "add", "--dev", "--quiet", "--offline", dep],
check=True,
)


def remove_dev_deps(pypi_names: list[str], *, offline: bool) -> None:
"""Remove the tool's development dependencies, if present."""
existing_dev_deps = get_dev_deps()

for dep in pypi_names:
if _strip_extras(dep) not in existing_dev_deps:
# Early exit; the tool is already not a dev dependency.
continue

console.print(
f"✔ Removing '{dep}' as a development dependency.",
style="green",
)
if not offline:
subprocess.run(
["uv", "remove", "--dev", "--quiet", _strip_extras(dep)], check=True
)
else:
subprocess.run(
["uv", "remove", "--dev", "--quiet", "--offline", _strip_extras(dep)],
check=True,
)


def is_dep_used(dep: str) -> bool:
return _strip_extras(dep) in get_dev_deps()


def _strip_extras(dep: str) -> str:
"""Remove extras from a dependency string."""
return re.sub(r"\[.*\]", "", dep)
12 changes: 10 additions & 2 deletions src/usethis/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import typer

from usethis import console, offline_opt
from usethis._bitbucket.config import (
add_bitbucket_pipeline_config,
remove_bitbucket_pipeline_config,
Expand All @@ -18,11 +19,14 @@ def bitbucket(
remove: bool = typer.Option(
False, "--remove", help="Remove Bitbucket pipelines CI instead of adding it."
),
offline: bool = offline_opt,
) -> None:
_bitbucket(remove=remove)
_bitbucket(remove=remove, offline=offline)


def _bitbucket(*, remove: bool = False) -> None:
def _bitbucket(*, remove: bool = False, offline: bool = False) -> None:
_ = offline # Already offline

config_yaml_path = Path.cwd() / "bitbucket-pipelines.yml"

if config_yaml_path.exists():
Expand Down Expand Up @@ -70,4 +74,8 @@ def _bitbucket(*, remove: bool = False) -> None:
)
)

console.print("☐ Populate the placeholder step in 'bitbucket-pipelines.yml'.")

add_steps(steps, is_parallel=True)

console.print("☐ Run your first pipeline on the Bitbucket website.")
Loading