Skip to content

Commit

Permalink
Merge pull request pypa/distutils#274 from msys2-contrib/ci-msvc-pyth…
Browse files Browse the repository at this point in the history
…on-mingw-variant2

mingw: make get_msvcr() a noop + add a CI job testing MSVC Python with GCC
  • Loading branch information
jaraco committed Aug 2, 2024
2 parents 6748224 + 1f999b9 commit 5f79f22
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 225 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,28 @@ jobs:
source /tmp/venv/bin/activate
pytest
test_msvc_python_mingw:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: 3.12
- name: Install tox
run: python -m pip install tox
- name: Install GCC
uses: msys2/setup-msys2@v2
with:
msystem: ucrt64
install: mingw-w64-ucrt-x86_64-cc
- name: Run
run: |
$env:MSYS2_ROOT = msys2 -c 'cygpath -m /'
$env:PATH = "$env:MSYS2_ROOT/ucrt64/bin;$env:PATH"
$env:DISTUTILS_TEST_DEFAULT_COMPILER = "mingw32"
tox
ci_setuptools:
# Integration testing with setuptools
strategy:
Expand Down
23 changes: 23 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,26 @@ def disable_macos_customization(monkeypatch):
from distutils import sysconfig

monkeypatch.setattr(sysconfig, '_customize_macos', lambda: None)


@pytest.fixture(autouse=True, scope="session")
def monkey_patch_get_default_compiler():
"""
Monkey patch distutils get_default_compiler to allow overriding the
default compiler. Mainly to test mingw32 with a MSVC Python.
"""
from distutils import ccompiler

default_compiler = os.environ.get("DISTUTILS_TEST_DEFAULT_COMPILER")

if default_compiler is not None:

def patched_get_default_compiler(*args, **kwargs):
return default_compiler

original = ccompiler.get_default_compiler
ccompiler.get_default_compiler = patched_get_default_compiler
yield
ccompiler.get_default_compiler = original
else:
yield
145 changes: 0 additions & 145 deletions distutils/_collections.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from __future__ import annotations

import collections
import functools
import itertools
import operator
from collections.abc import Mapping
from typing import Any


# from jaraco.collections 3.5.1
Expand Down Expand Up @@ -60,144 +56,3 @@ def __contains__(self, other):

def __len__(self):
return len(list(iter(self)))


# from jaraco.collections 5.0.1
class RangeMap(dict):
"""
A dictionary-like object that uses the keys as bounds for a range.
Inclusion of the value for that range is determined by the
key_match_comparator, which defaults to less-than-or-equal.
A value is returned for a key if it is the first key that matches in
the sorted list of keys.
One may supply keyword parameters to be passed to the sort function used
to sort keys (i.e. key, reverse) as sort_params.
Create a map that maps 1-3 -> 'a', 4-6 -> 'b'
>>> r = RangeMap({3: 'a', 6: 'b'}) # boy, that was easy
>>> r[1], r[2], r[3], r[4], r[5], r[6]
('a', 'a', 'a', 'b', 'b', 'b')
Even float values should work so long as the comparison operator
supports it.
>>> r[4.5]
'b'
Notice that the way rangemap is defined, it must be open-ended
on one side.
>>> r[0]
'a'
>>> r[-1]
'a'
One can close the open-end of the RangeMap by using undefined_value
>>> r = RangeMap({0: RangeMap.undefined_value, 3: 'a', 6: 'b'})
>>> r[0]
Traceback (most recent call last):
...
KeyError: 0
One can get the first or last elements in the range by using RangeMap.Item
>>> last_item = RangeMap.Item(-1)
>>> r[last_item]
'b'
.last_item is a shortcut for Item(-1)
>>> r[RangeMap.last_item]
'b'
Sometimes it's useful to find the bounds for a RangeMap
>>> r.bounds()
(0, 6)
RangeMap supports .get(key, default)
>>> r.get(0, 'not found')
'not found'
>>> r.get(7, 'not found')
'not found'
One often wishes to define the ranges by their left-most values,
which requires use of sort params and a key_match_comparator.
>>> r = RangeMap({1: 'a', 4: 'b'},
... sort_params=dict(reverse=True),
... key_match_comparator=operator.ge)
>>> r[1], r[2], r[3], r[4], r[5], r[6]
('a', 'a', 'a', 'b', 'b', 'b')
That wasn't nearly as easy as before, so an alternate constructor
is provided:
>>> r = RangeMap.left({1: 'a', 4: 'b', 7: RangeMap.undefined_value})
>>> r[1], r[2], r[3], r[4], r[5], r[6]
('a', 'a', 'a', 'b', 'b', 'b')
"""

def __init__(
self,
source,
sort_params: Mapping[str, Any] = {},
key_match_comparator=operator.le,
):
dict.__init__(self, source)
self.sort_params = sort_params
self.match = key_match_comparator

@classmethod
def left(cls, source):
return cls(
source, sort_params=dict(reverse=True), key_match_comparator=operator.ge
)

def __getitem__(self, item):
sorted_keys = sorted(self.keys(), **self.sort_params)
if isinstance(item, RangeMap.Item):
result = self.__getitem__(sorted_keys[item])
else:
key = self._find_first_match_(sorted_keys, item)
result = dict.__getitem__(self, key)
if result is RangeMap.undefined_value:
raise KeyError(key)
return result

def get(self, key, default=None):
"""
Return the value for key if key is in the dictionary, else default.
If default is not given, it defaults to None, so that this method
never raises a KeyError.
"""
try:
return self[key]
except KeyError:
return default

def _find_first_match_(self, keys, item):
is_match = functools.partial(self.match, item)
matches = list(filter(is_match, keys))
if matches:
return matches[0]
raise KeyError(item)

def bounds(self):
sorted_keys = sorted(self.keys(), **self.sort_params)
return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item])

# some special values for the RangeMap
undefined_value = type('RangeValueUndefined', (), {})()

class Item(int):
"RangeMap Item"

first_item = Item(0)
last_item = Item(-1)
40 changes: 2 additions & 38 deletions distutils/cygwinccompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
import copy
import os
import pathlib
import re
import shlex
import sys
import warnings
from subprocess import check_output

from ._collections import RangeMap
from .errors import (
CCompilerError,
CompileError,
Expand All @@ -26,42 +24,10 @@
from .unixccompiler import UnixCCompiler
from .version import LooseVersion, suppress_known_deprecation

_msvcr_lookup = RangeMap.left(
{
# MSVC 7.0
1300: ['msvcr70'],
# MSVC 7.1
1310: ['msvcr71'],
# VS2005 / MSVC 8.0
1400: ['msvcr80'],
# VS2008 / MSVC 9.0
1500: ['msvcr90'],
# VS2010 / MSVC 10.0
1600: ['msvcr100'],
# VS2012 / MSVC 11.0
1700: ['msvcr110'],
# VS2013 / MSVC 12.0
1800: ['msvcr120'],
# VS2015 / MSVC 14.0
1900: ['vcruntime140'],
2000: RangeMap.undefined_value,
},
)


def get_msvcr():
"""Include the appropriate MSVC runtime library if Python was built
with MSVC 7.0 or later.
"""
match = re.search(r'MSC v\.(\d{4})', sys.version)
try:
msc_ver = int(match.group(1))
except AttributeError:
return []
try:
return _msvcr_lookup[msc_ver]
except KeyError:
raise ValueError(f"Unknown MS Compiler version {msc_ver} ")
"""No longer needed, but kept for backward compatibility."""
return []


_runtime_library_dirs_msg = (
Expand Down Expand Up @@ -113,8 +79,6 @@ def __init__(self, verbose=False, dry_run=False, force=False):
linker_so_cxx=f'{self.linker_dll_cxx} -mcygwin {shared_option}',
)

# Include the appropriate MSVC runtime library if Python was built
# with MSVC 7.0 or later.
self.dll_libraries = get_msvcr()

@property
Expand Down
42 changes: 0 additions & 42 deletions distutils/tests/test_cygwinccompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,50 +71,8 @@ def test_check_config_h(self):
assert check_config_h()[0] == CONFIG_H_OK

def test_get_msvcr(self):
# []
sys.version = (
'2.6.1 (r261:67515, Dec 6 2008, 16:42:21) '
'\n[GCC 4.0.1 (Apple Computer, Inc. build 5370)]'
)
assert get_msvcr() == []

# MSVC 7.0
sys.version = (
'2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1300 32 bits (Intel)]'
)
assert get_msvcr() == ['msvcr70']

# MSVC 7.1
sys.version = (
'2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bits (Intel)]'
)
assert get_msvcr() == ['msvcr71']

# VS2005 / MSVC 8.0
sys.version = (
'2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1400 32 bits (Intel)]'
)
assert get_msvcr() == ['msvcr80']

# VS2008 / MSVC 9.0
sys.version = (
'2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1500 32 bits (Intel)]'
)
assert get_msvcr() == ['msvcr90']

sys.version = (
'3.10.0 (tags/v3.10.0:b494f59, Oct 4 2021, 18:46:30) '
'[MSC v.1929 32 bit (Intel)]'
)
assert get_msvcr() == ['vcruntime140']

# unknown
sys.version = (
'2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.2000 32 bits (Intel)]'
)
with pytest.raises(ValueError):
get_msvcr()

@pytest.mark.skipif('sys.platform != "cygwin"')
def test_dll_libraries_not_none(self):
from distutils.cygwinccompiler import CygwinCCompiler
Expand Down
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ setenv =
PYTHONWARNDEFAULTENCODING = 1
# pypa/distutils#99
VIRTUALENV_NO_SETUPTOOLS = 1
pass_env =
DISTUTILS_TEST_DEFAULT_COMPILER
commands =
pytest {posargs}
usedevelop = True
Expand Down

0 comments on commit 5f79f22

Please sign in to comment.