Skip to content

Commit

Permalink
Make Go a first class language
Browse files Browse the repository at this point in the history
  • Loading branch information
taoufik07 committed Jan 8, 2023
1 parent dc667ab commit dac33b0
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 17 deletions.
107 changes: 98 additions & 9 deletions pre_commit/languages/golang.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from __future__ import annotations

import contextlib
import functools
import json
import os.path
import platform
import shutil
import sys
import tempfile
import urllib.error
import urllib.request
from typing import Generator
from typing import Sequence
from urllib.parse import urlsplit

import pre_commit.constants as C
from pre_commit.envcontext import envcontext
Expand All @@ -17,20 +25,92 @@
from pre_commit.util import rmtree

ENVIRONMENT_DIR = 'golangenv'
get_default_version = helpers.basic_get_default_version
health_check = helpers.basic_health_check


def get_env_patch(venv: str) -> PatchesT:
@functools.lru_cache(maxsize=1)
def get_default_version() -> str:
if helpers.exe_exists('go'):
return 'system'
else:
return C.DEFAULT


def get_env_patch(venv: str, language_version: str) -> PatchesT:
if language_version == 'system':
return (
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
)

return (
('GOROOT', os.path.join(venv, '.go')),
(
'PATH', (
os.path.join(venv, 'bin'), os.pathsep,
os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'),
),
),
)


def _get_arch() -> str: # pragma: no cover
arch = platform.machine().lower()
_ALIASES = {
'x86_64': 'amd64',
'i386': '386',
'aarch64': 'arm64',
'armv8': 'arm64',
'armv7l': 'armv6l',
}
return _ALIASES.get(arch, arch)


@functools.lru_cache
def _infer_go_version(version: str) -> str:
if version != C.DEFAULT:
return version
resp = urllib.request.urlopen('https://go.dev/dl/?mode=json')
return json.loads(resp.read())[0]['version'].lstrip('go')


def _get_url(version: str) -> str:
os_name = platform.system().lower()
ext = 'zip' if os_name == 'windows' else 'tar.gz'
version = _infer_go_version(version)
return (
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
f'https://dl.google.com/go/go{version}.{os_name}-{_get_arch()}.{ext}'
)


def _install_go(version: str, dest: str) -> None:
try:
url = _get_url(version)
resp = urllib.request.urlopen(url)
except urllib.error.HTTPError as exec: # pragma: no cover
if exec.code == 404:
raise ValueError(
'Could not find a version matching your system requirements '
f'(os={platform.system().lower()}; arch={_get_arch()})',
) from exec
else:
raise
else:
with tempfile.NamedTemporaryFile(
delete=False,
suffix=f'_{os.path.basename(urlsplit(url).path)}',
) as tmp_file:
shutil.copyfileobj(resp, tmp_file)
shutil.unpack_archive(tmp_file.name, dest)
shutil.move(os.path.join(dest, 'go'), os.path.join(dest, '.go'))


@contextlib.contextmanager
def in_env(prefix: Prefix) -> Generator[None, None, None]:
envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT)
with envcontext(get_env_patch(envdir)):
def in_env(
prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, language_version)
with envcontext(get_env_patch(envdir, language_version)):
yield


Expand All @@ -39,15 +119,24 @@ def install_environment(
version: str,
additional_dependencies: Sequence[str],
) -> None:
helpers.assert_version_default('golang', version)
env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)

if version != 'system':
_install_go(version, env_dir)

if sys.platform == 'cygwin': # pragma: no cover
gopath = cmd_output('cygpath', '-w', env_dir)[1].strip()
else:
gopath = env_dir
gopath = os.path.join(env_dir)

env = dict(os.environ, GOPATH=gopath)
env.pop('GOBIN', None)
if version != 'system':
env['GOROOT'] = os.path.join(env_dir, '.go')
env['PATH'] = (
os.path.join(env_dir, '.go', 'bin') +
os.pathsep + os.environ['PATH']
)

helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env)
for dependency in additional_dependencies:
Expand All @@ -64,5 +153,5 @@ def run_hook(
file_args: Sequence[str],
color: bool,
) -> tuple[int, bytes]:
with in_env(hook.prefix):
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ package main

import (
"fmt"
"runtime"
"github.com/BurntSushi/toml"
"os"
)

type Config struct {
What string
}

func main() {
version := runtime.Version()
if len(os.Args) > 1 {
version = os.Args[1]
}
var conf Config
toml.Decode("What = 'world'\n", &conf)
fmt.Printf("hello %v\n", conf.What)
fmt.Printf("hello %v from %s\n", conf.What, version)
}
45 changes: 45 additions & 0 deletions tests/languages/golang_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

import urllib.request
from unittest import mock

import pytest

import pre_commit.constants as C
from pre_commit.languages import golang
from pre_commit.languages import helpers


ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__


@pytest.fixture
def exe_exists_mck():
with mock.patch.object(helpers, 'exe_exists') as mck:
yield mck


def test_golang_default_version_system_available(exe_exists_mck):
exe_exists_mck.return_value = True
assert ACTUAL_GET_DEFAULT_VERSION() == 'system'


def test_golang_default_version_system_not_available(exe_exists_mck):
exe_exists_mck.return_value = False
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT


ACTUAL_INFER_GO_VERSION = golang._infer_go_version.__wrapped__


def test_golang_infer_go_version_not_default():
assert ACTUAL_INFER_GO_VERSION('1.19.4') == '1.19.4'


def test_golang_infer_go_version_default():
version = ACTUAL_INFER_GO_VERSION(C.DEFAULT)
assert version != C.DEFAULT

golang.get_default_version.cache_clear()
resp = urllib.request.urlopen(golang._get_url(version))
assert resp.code == 200
74 changes: 67 additions & 7 deletions tests/repository_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,42 @@ def _get_hook(repo_config, store, hook_id):
return hook


def _test_hook_repo(
def _try_hook_repo(
tempdir_factory,
store,
repo_path,
hook_id,
args,
expected,
expected_return_code=0,
config_kwargs=None,
color=False,
):
path = make_repo(tempdir_factory, repo_path)
config = make_config_from_repo(path, **(config_kwargs or {}))
hook = _get_hook(config, store, hook_id)
ret, out = _hook_run(hook, args, color=color)
return ret, out


def _test_hook_repo(
tempdir_factory,
store,
repo_path,
hook_id,
args,
expected,
expected_return_code=0,
config_kwargs=None,
color=False,
):
ret, out = _try_hook_repo(
tempdir_factory,
store,
repo_path,
hook_id,
args,
config_kwargs=config_kwargs,
color=color,
)
assert ret == expected_return_code
assert _norm_out(out) == expected

Expand Down Expand Up @@ -380,17 +401,54 @@ def test_swift_hook(tempdir_factory, store):
)


def test_golang_hook(tempdir_factory, store):
@pytest.fixture
def _golang_lru_cache_clear():
golang.get_default_version.cache_clear()
yield
golang.get_default_version.cache_clear()


@pytest.mark.usefixtures('_golang_lru_cache_clear')
def test_golang_default_to_system_hook(tempdir_factory, store):
with mock.patch.object(golang, '_install_go') as install_go_mck:
ret, out = _try_hook_repo(
tempdir_factory, store, 'golang_hooks_repo',
'golang-hook', [],
)
assert ret == 0
install_go_mck.assert_not_called()


@pytest.mark.usefixtures('_golang_lru_cache_clear')
def test_golang_default_to_version_hook(tempdir_factory, store):
with mock.patch.object(helpers, 'exe_exists') as exe_exists_mck:
with mock.patch.object(golang, '_infer_go_version') as _infer_go_mck:
exe_exists_mck.return_value = False
_infer_go_mck.return_value = '1.18.4'
_test_hook_repo(
tempdir_factory, store, 'golang_hooks_repo',
'golang-hook', [], b'hello world from go1.18.4\n',
)
_infer_go_mck.assert_called_once_with(C.DEFAULT)


def test_golang_versioned_hook(tempdir_factory, store):
_test_hook_repo(
tempdir_factory, store, 'golang_hooks_repo',
'golang-hook', [], b'hello world\n',
'golang-hook', [], b'hello world from go1.18.4\n',
config_kwargs={
'hooks': [{
'id': 'golang-hook',
'language_version': '1.18.4',
}],
},
)


def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store):
gobin_dir = tempdir_factory.get()
with envcontext((('GOBIN', gobin_dir),)):
test_golang_hook(tempdir_factory, store)
test_golang_default_to_system_hook(tempdir_factory, store)
assert os.listdir(gobin_dir) == []


Expand Down Expand Up @@ -665,6 +723,7 @@ def test_additional_node_dependencies_installed(tempdir_factory, store):
assert 'lodash' in output


@pytest.mark.usefixtures('_golang_lru_cache_clear')
def test_additional_golang_dependencies_installed(
tempdir_factory, store,
):
Expand All @@ -677,14 +736,15 @@ def test_additional_golang_dependencies_installed(
envdir = helpers.environment_dir(
hook.prefix,
golang.ENVIRONMENT_DIR,
C.DEFAULT,
golang.get_default_version(),
)
binaries = os.listdir(os.path.join(envdir, 'bin'))
# normalize for windows
binaries = [os.path.splitext(binary)[0] for binary in binaries]
assert 'hello' in binaries


@pytest.mark.usefixtures('_golang_lru_cache_clear')
def test_local_golang_additional_dependencies(store):
config = {
'repo': 'local',
Expand Down

0 comments on commit dac33b0

Please sign in to comment.