Skip to content

Commit

Permalink
Add support for Python 3.8. (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsirois authored Dec 29, 2024
1 parent 79c1b3d commit 45d71ce
Show file tree
Hide file tree
Showing 19 changed files with 225 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
strategy:
matrix:
include:
- name: "Linux Python 3.8"
os: "ubuntu-24.04"
python-version: "3.8"
- name: "Linux Python 3.9"
os: "ubuntu-24.04"
python-version: "3.9"
Expand All @@ -47,9 +50,11 @@ jobs:
- name: "Linux Python 3.13"
os: "ubuntu-24.04"
python-version: "3.13"

- name: "Mac Python 3.13"
os: "macos-14"
python-version: "3.13"

- name: "Windows Python 3.13"
os: "windows-2022"
python-version: "3.13"
Expand Down
7 changes: 6 additions & 1 deletion python/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@
__pycache__/

# UV outputs artifacts here.
/dist/
/dist/

# Tool caches.
.mypy_cache/
.pytest_cache/
.ruff_cache/
6 changes: 6 additions & 0 deletions python/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# insta-science

## 0.3.0

Add support for Python 3.8.

Also respect color settings when printing error output.

## 0.2.1

Fix the `insta_science.ensure_installed` API to not exit on error.
Expand Down
8 changes: 8 additions & 0 deletions python/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include uv.lock

graft scripts
graft test-support
graft tests
prune tests/.pytest_cache

global-exclude *.pyc *.pyo
2 changes: 2 additions & 0 deletions python/insta_science/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from ._internal import (
CURRENT_PLATFORM,
Digest,
Fingerprint,
InputError,
Expand All @@ -13,6 +14,7 @@
from .version import __version__

__all__ = (
"CURRENT_PLATFORM",
"Digest",
"Fingerprint",
"InputError",
Expand Down
62 changes: 62 additions & 0 deletions python/insta_science/_colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2024 Science project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import annotations

import os
import sys
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Iterator


@dataclass(frozen=True)
class Colors:
use_color: bool

def red(self, text) -> str:
return self.color(text, fg="red")

def yellow(self, text) -> str:
return self.color(text, fg="yellow")

def color(self, text, fg: str):
if not self.use_color:
return text

import colors

return colors.color(text, fg=fg)


@contextmanager
def color_support(use_color: bool | None = None) -> Iterator[Colors]:
if use_color in (True, None):
try:
import colorama
except ImportError:
pass
else:
colorama.just_fix_windows_console()

if use_color is None:

def _use_color() -> bool:
# Used in Python 3.13+
python_colors = os.environ.get("PYTHON_COLORS")
if python_colors in ("0", "1"):
return python_colors == "1"

# A common convention; see: https://no-color.org/
if "NO_COLOR" in os.environ:
return False

# A less common convention; see: https://force-color.org/
if "FORCE_COLOR" in os.environ:
return True

return sys.stderr.isatty() and "dumb" != os.environ.get("TERM")

use_color = _use_color()

yield Colors(use_color=use_color)
3 changes: 2 additions & 1 deletion python/insta_science/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
from .errors import InputError, ScienceNotFound
from .hashing import Fingerprint
from .model import Digest, Science
from .platform import Platform
from .platform import CURRENT_PLATFORM, Platform
from .science import ensure_installed

__all__ = (
"CURRENT_PLATFORM",
"Digest",
"Fingerprint",
"InputError",
Expand Down
6 changes: 3 additions & 3 deletions python/insta_science/_internal/a_scie.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .fetcher import fetch_and_verify
from .hashing import Digest, Fingerprint
from .model import Science, Url
from .platform import Platform
from .platform import CURRENT_PLATFORM, Platform


@dataclass(frozen=True)
Expand All @@ -26,7 +26,7 @@ def _load_project_release(
binary_name: str,
version: Version | None = None,
fingerprint: Digest | Fingerprint | None = None,
platform: Platform = Platform.current(),
platform: Platform = CURRENT_PLATFORM,
) -> _LoadResult:
qualified_binary_name = platform.qualified_binary_name(binary_name)
base_url = f"https://github.com/a-scie/{project_name}/releases"
Expand All @@ -45,7 +45,7 @@ def _load_project_release(
return _LoadResult(path=path, binary_name=qualified_binary_name)


def science(spec: Science | None = None, platform: Platform = Platform.current()) -> PurePath:
def science(spec: Science | None = None, platform: Platform = CURRENT_PLATFORM) -> PurePath:
version = spec.version if spec else None
fingerprint = spec.digest if spec and spec.digest else None
return _load_project_release(
Expand Down
11 changes: 4 additions & 7 deletions python/insta_science/_internal/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import cache
from pathlib import Path
from typing import Iterator, Union

Expand Down Expand Up @@ -83,10 +82,8 @@ def get_or_create(self, url: str, ttl: timedelta | None = None) -> Iterator[Cach
ttl_file.write_text((datetime.now() + ttl).strftime(_TTL_EXPIRY_FORMAT))


@cache
def download_cache() -> DownloadCache:
return DownloadCache(
base_dir=Path(
os.environ.get("INSTA_SCIENCE_CACHE", appdirs.user_cache_dir(appname="insta-science"))
)
DOWNLOAD_CACHE = DownloadCache(
base_dir=Path(
os.environ.get("INSTA_SCIENCE_CACHE", appdirs.user_cache_dir(appname="insta-science"))
)
)
13 changes: 6 additions & 7 deletions python/insta_science/_internal/fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from tqdm import tqdm

from . import hashing
from .cache import Missing, download_cache
from .cache import DOWNLOAD_CACHE, Missing
from .errors import InputError
from .hashing import Digest, ExpectedDigest, Fingerprint
from .model import Url
Expand Down Expand Up @@ -92,12 +92,11 @@ def _configured_client(url: Url, headers: Mapping[str, str] | None = None) -> ht
def _fetch_to_cache(
url: Url, ttl: timedelta | None = None, headers: Mapping[str, str] | None = None
) -> Path:
with download_cache().get_or_create(url, ttl=ttl) as cache_result:
with DOWNLOAD_CACHE.get_or_create(url, ttl=ttl) as cache_result:
if isinstance(cache_result, Missing):
with (
_configured_client(url, headers).stream("GET", url) as response,
cache_result.work.open("wb") as cache_fp,
):
with _configured_client(url, headers).stream(
"GET", url
) as response, cache_result.work.open("wb") as cache_fp:
for data in response.iter_bytes():
cache_fp.write(data)
return cache_result.path
Expand Down Expand Up @@ -150,7 +149,7 @@ def fetch_and_verify(
headers: Mapping[str, str] | None = None,
) -> PurePath:
verified_fingerprint = False
with download_cache().get_or_create(url, ttl=ttl) as cache_result:
with DOWNLOAD_CACHE.get_or_create(url, ttl=ttl) as cache_result:
if isinstance(cache_result, Missing):
# TODO(John Sirois): XXX: Log or invoke callback for logging.
# click.secho(f"Downloading {url} ...", fg="green")
Expand Down
5 changes: 3 additions & 2 deletions python/insta_science/_internal/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import platform
from enum import Enum
from functools import cache

from .errors import InputError

Expand All @@ -22,7 +21,6 @@ class Platform(Enum):
Windows_x86_64 = "windows-x86_64"

@classmethod
@cache
def current(cls) -> Platform:
system = platform.system().lower()
machine = platform.machine().lower()
Expand Down Expand Up @@ -69,3 +67,6 @@ def binary_name(self, binary_name: str) -> str:

def qualified_binary_name(self, binary_name: str) -> str:
return f"{binary_name}-{self.value}{self.extension}"


CURRENT_PLATFORM = Platform.current()
15 changes: 7 additions & 8 deletions python/insta_science/_internal/science.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
from packaging.version import Version

from . import a_scie, parser, project
from .cache import Missing, download_cache
from .cache import DOWNLOAD_CACHE, Missing
from .errors import InputError, ScienceNotFound
from .hashing import ExpectedDigest
from .model import Science
from .platform import Platform
from .platform import CURRENT_PLATFORM


def _find_science_on_path(spec: Science) -> PurePath | None:
Expand All @@ -30,14 +30,13 @@ def _find_science_on_path(spec: Science) -> PurePath | None:
else:
ttl = timedelta(days=5)

with download_cache().get_or_create(url=url, ttl=ttl) as cache_result:
with DOWNLOAD_CACHE.get_or_create(url=url, ttl=ttl) as cache_result:
if isinstance(cache_result, Missing):
current_platform = Platform.current()
for binary_name in (
current_platform.binary_name("science"),
current_platform.binary_name("science-fat"),
current_platform.qualified_binary_name("science"),
current_platform.qualified_binary_name("science-fat"),
CURRENT_PLATFORM.binary_name("science"),
CURRENT_PLATFORM.binary_name("science-fat"),
CURRENT_PLATFORM.qualified_binary_name("science"),
CURRENT_PLATFORM.qualified_binary_name("science-fat"),
):
science_exe = shutil.which(binary_name)
if not science_exe:
Expand Down
16 changes: 9 additions & 7 deletions python/insta_science/shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,26 @@
import sys
from typing import NoReturn

import colors

from . import InputError, Platform, ScienceNotFound, ensure_installed
from . import CURRENT_PLATFORM, InputError, ScienceNotFound, ensure_installed
from ._colors import color_support


def main() -> NoReturn:
try:
science_exe = ensure_installed()
except InputError as e:
sys.exit(f"{colors.red('Configuration error')}: {colors.yellow(str(e))}")
with color_support() as colors:
sys.exit(f"{colors.red('Configuration error')}: {colors.yellow(str(e))}")
except ScienceNotFound as e:
sys.exit(colors.red(str(e)))
with color_support() as colors:
sys.exit(colors.red(str(e)))

argv = [str(science_exe), *sys.argv[1:]]
try:
if Platform.current().is_windows:
if CURRENT_PLATFORM.is_windows:
sys.exit(subprocess.run(argv).returncode)
else:
os.execv(science_exe, argv)
except OSError as e:
sys.exit(colors.red(str(e)))
with color_support() as colors:
sys.exit(colors.red(str(e)))
13 changes: 9 additions & 4 deletions python/insta_science/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

from typing import Any

from ._colors import color_support


def main() -> Any:
raise NotImplementedError(
"TODO(John Sirois): implement download subcommand for seeding offline `science` binary "
"access."
)
with color_support() as colors:
raise NotImplementedError(
colors.yellow(
"TODO(John Sirois): implement download subcommand for seeding offline `science` "
"binary access."
)
)
2 changes: 1 addition & 1 deletion python/insta_science/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2024 Science project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

__version__ = "0.2.1"
__version__ = "0.3.0"
Loading

0 comments on commit 45d71ce

Please sign in to comment.