Skip to content

Commit

Permalink
Merge branch 'main' into feature/skeleton
Browse files Browse the repository at this point in the history
  • Loading branch information
jaraco committed Nov 5, 2023
2 parents fe9018c + 6039322 commit cf6fb0c
Show file tree
Hide file tree
Showing 16 changed files with 252 additions and 123 deletions.
53 changes: 53 additions & 0 deletions distutils/_functools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import collections.abc
import functools


Expand All @@ -18,3 +19,55 @@ def wrapper(param, *args, **kwargs):
return func(param, *args, **kwargs)

return wrapper


# from jaraco.functools 4.0
@functools.singledispatch
def _splat_inner(args, func):
"""Splat args to func."""
return func(*args)


@_splat_inner.register
def _(args: collections.abc.Mapping, func):
"""Splat kargs to func as kwargs."""
return func(**args)


def splat(func):
"""
Wrap func to expect its parameters to be passed positionally in a tuple.
Has a similar effect to that of ``itertools.starmap`` over
simple ``map``.
>>> import itertools, operator
>>> pairs = [(-1, 1), (0, 2)]
>>> _ = tuple(itertools.starmap(print, pairs))
-1 1
0 2
>>> _ = tuple(map(splat(print), pairs))
-1 1
0 2
The approach generalizes to other iterators that don't have a "star"
equivalent, such as a "starfilter".
>>> list(filter(splat(operator.add), pairs))
[(0, 2)]
Splat also accepts a mapping argument.
>>> def is_nice(msg, code):
... return "smile" in msg or code == 0
>>> msgs = [
... dict(msg='smile!', code=20),
... dict(msg='error :(', code=1),
... dict(msg='unknown', code=0),
... ]
>>> for msg in filter(splat(is_nice), msgs):
... print(msg)
{'msg': 'smile!', 'code': 20}
{'msg': 'unknown', 'code': 0}
"""
return functools.wraps(func)(functools.partial(_splat_inner, func=func))
72 changes: 72 additions & 0 deletions distutils/_modified.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Timestamp comparison of files and groups of files."""

import functools
import os.path

from .errors import DistutilsFileError
from .py39compat import zip_strict
from ._functools import splat


def _newer(source, target):
return not os.path.exists(target) or (
os.path.getmtime(source) > os.path.getmtime(target)
)


def newer(source, target):
"""
Is source modified more recently than target.
Returns True if 'source' is modified more recently than
'target' or if 'target' does not exist.
Raises DistutilsFileError if 'source' does not exist.
"""
if not os.path.exists(source):
raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source))

return _newer(source, target)


def newer_pairwise(sources, targets, newer=newer):
"""
Filter filenames where sources are newer than targets.
Walk two filename iterables in parallel, testing if each source is newer
than its corresponding target. Returns a pair of lists (sources,
targets) where source is newer than target, according to the semantics
of 'newer()'.
"""
newer_pairs = filter(splat(newer), zip_strict(sources, targets))
return tuple(map(list, zip(*newer_pairs))) or ([], [])


def newer_group(sources, target, missing='error'):
"""
Is target out-of-date with respect to any file in sources.
Return True if 'target' is out-of-date with respect to any file
listed in 'sources'. In other words, if 'target' exists and is newer
than every file in 'sources', return False; otherwise return True.
``missing`` controls how to handle a missing source file:
- error (default): allow the ``stat()`` call to fail.
- ignore: silently disregard any missing source files.
- newer: treat missing source files as "target out of date". This
mode is handy in "dry-run" mode: it will pretend to carry out
commands that wouldn't work because inputs are missing, but
that doesn't matter because dry-run won't run the commands.
"""

def missing_as_newer(source):
return missing == 'newer' and not os.path.exists(source)

ignored = os.path.exists if missing == 'ignore' else None
return any(
missing_as_newer(source) or _newer(source, target)
for source in filter(ignored, sources)
)


newer_pairwise_group = functools.partial(newer_pairwise, newer=newer_group)
2 changes: 1 addition & 1 deletion distutils/bcppcompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
)
from .ccompiler import CCompiler, gen_preprocess_options
from .file_util import write_file
from .dep_util import newer
from ._modified import newer
from ._log import log


Expand Down
2 changes: 1 addition & 1 deletion distutils/ccompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .spawn import spawn
from .file_util import move_file
from .dir_util import mkpath
from .dep_util import newer_group
from ._modified import newer_group
from .util import split_quoted, execute
from ._log import log

Expand Down
4 changes: 2 additions & 2 deletions distutils/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import logging

from .errors import DistutilsOptionError
from . import util, dir_util, file_util, archive_util, dep_util
from . import util, dir_util, file_util, archive_util, _modified
from ._log import log


Expand Down Expand Up @@ -428,7 +428,7 @@ def make_file(
# If 'outfile' must be regenerated (either because it doesn't
# exist, is out-of-date, or the 'force' flag is true) then
# perform the action that presumably regenerates it
if self.force or dep_util.newer_group(infiles, outfile):
if self.force or _modified.newer_group(infiles, outfile):
self.execute(func, args, exec_msg, level)
# Otherwise, print the "skip" message
else:
Expand Down
2 changes: 1 addition & 1 deletion distutils/command/build_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)
from ..sysconfig import customize_compiler, get_python_version
from ..sysconfig import get_config_h_filename
from ..dep_util import newer_group
from .._modified import newer_group
from ..extension import Extension
from ..util import get_platform
from distutils._log import log
Expand Down
2 changes: 1 addition & 1 deletion distutils/command/build_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from stat import ST_MODE
from distutils import sysconfig
from ..core import Command
from ..dep_util import newer
from .._modified import newer
from ..util import convert_path
from distutils._log import log
import tokenize
Expand Down
104 changes: 11 additions & 93 deletions distutils/dep_util.py
Original file line number Diff line number Diff line change
@@ -1,96 +1,14 @@
"""distutils.dep_util
import warnings

Utility functions for simple, timestamp-based dependency of files
and groups of files; also, function based entirely on such
timestamp dependency analysis."""
from . import _modified

import os
from .errors import DistutilsFileError


def newer(source, target):
"""Return true if 'source' exists and is more recently modified than
'target', or if 'source' exists and 'target' doesn't. Return false if
both exist and 'target' is the same age or younger than 'source'.
Raise DistutilsFileError if 'source' does not exist.
"""
if not os.path.exists(source):
raise DistutilsFileError("file '%s' does not exist" % os.path.abspath(source))
if not os.path.exists(target):
return 1

from stat import ST_MTIME

mtime1 = os.stat(source)[ST_MTIME]
mtime2 = os.stat(target)[ST_MTIME]

return mtime1 > mtime2


# newer ()


def newer_pairwise(sources, targets):
"""Walk two filename lists in parallel, testing if each source is newer
than its corresponding target. Return a pair of lists (sources,
targets) where source is newer than target, according to the semantics
of 'newer()'.
"""
if len(sources) != len(targets):
raise ValueError("'sources' and 'targets' must be same length")

# build a pair of lists (sources, targets) where source is newer
n_sources = []
n_targets = []
for i in range(len(sources)):
if newer(sources[i], targets[i]):
n_sources.append(sources[i])
n_targets.append(targets[i])

return (n_sources, n_targets)


# newer_pairwise ()


def newer_group(sources, target, missing='error'):
"""Return true if 'target' is out-of-date with respect to any file
listed in 'sources'. In other words, if 'target' exists and is newer
than every file in 'sources', return false; otherwise return true.
'missing' controls what we do when a source file is missing; the
default ("error") is to blow up with an OSError from inside 'stat()';
if it is "ignore", we silently drop any missing source files; if it is
"newer", any missing source files make us assume that 'target' is
out-of-date (this is handy in "dry-run" mode: it'll make you pretend to
carry out commands that wouldn't work because inputs are missing, but
that doesn't matter because you're not actually going to run the
commands).
"""
# If the target doesn't even exist, then it's definitely out-of-date.
if not os.path.exists(target):
return 1

# Otherwise we have to find out the hard way: if *any* source file
# is more recent than 'target', then 'target' is out-of-date and
# we can immediately return true. If we fall through to the end
# of the loop, then 'target' is up-to-date and we return false.
from stat import ST_MTIME

target_mtime = os.stat(target)[ST_MTIME]
for source in sources:
if not os.path.exists(source):
if missing == 'error': # blow up when we stat() the file
pass
elif missing == 'ignore': # missing source dropped from
continue # target's dependency list
elif missing == 'newer': # missing source means target is
return 1 # out-of-date

source_mtime = os.stat(source)[ST_MTIME]
if source_mtime > target_mtime:
return 1
else:
return 0


# newer_group ()
def __getattr__(name):
if name not in ['newer', 'newer_group', 'newer_pairwise']:
raise AttributeError(name)
warnings.warn(
"dep_util is Deprecated. Use functions from setuptools instead.",
DeprecationWarning,
stacklevel=2,
)
return getattr(_modified, name)
2 changes: 1 addition & 1 deletion distutils/file_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def copy_file( # noqa: C901
# changing it (ie. it's not already a hard/soft link to src OR
# (not update) and (src newer than dst).

from distutils.dep_util import newer
from distutils._modified import newer
from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE

if not os.path.isfile(src):
Expand Down
46 changes: 45 additions & 1 deletion distutils/py39compat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys
import functools
import itertools
import platform
import sys


def add_ext_suffix_39(vars):
Expand All @@ -20,3 +22,45 @@ def add_ext_suffix_39(vars):

needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows'
add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None


# from more_itertools
class UnequalIterablesError(ValueError):
def __init__(self, details=None):
msg = 'Iterables have different lengths'
if details is not None:
msg += (': index 0 has length {}; index {} has length {}').format(*details)

super().__init__(msg)


# from more_itertools
def _zip_equal_generator(iterables):
_marker = object()
for combo in itertools.zip_longest(*iterables, fillvalue=_marker):
for val in combo:
if val is _marker:
raise UnequalIterablesError()
yield combo


# from more_itertools
def _zip_equal(*iterables):
# Check whether the iterables are all the same size.
try:
first_size = len(iterables[0])
for i, it in enumerate(iterables[1:], 1):
size = len(it)
if size != first_size:
raise UnequalIterablesError(details=(first_size, i, size))
# All sizes are equal, we can use the built-in zip.
return zip(*iterables)
# If any one of the iterables didn't have a length, start reading
# them until one runs out.
except TypeError:
return _zip_equal_generator(iterables)


zip_strict = (
_zip_equal if sys.version_info < (3, 10) else functools.partial(zip, strict=True)
)
Loading

0 comments on commit cf6fb0c

Please sign in to comment.