diff --git a/distutils/command/install_data.py b/distutils/command/install_data.py index 624c0b90..bd2932ab 100644 --- a/distutils/command/install_data.py +++ b/distutils/command/install_data.py @@ -5,8 +5,13 @@ # contributed by Bastian Kleineidam +from __future__ import annotations + +import functools import os +from typing import Iterable + from ..core import Command from ..util import change_root, convert_path @@ -46,36 +51,42 @@ def finalize_options(self): def run(self): self.mkpath(self.install_dir) for f in self.data_files: - if isinstance(f, str): - # it's a simple file, so copy it - f = convert_path(f) - if self.warn_dir: - self.warn( - "setup script did not provide a directory for " - f"'{f}' -- installing right in '{self.install_dir}'" - ) - (out, _) = self.copy_file(f, self.install_dir) + self._copy(f) + + @functools.singledispatchmethod + def _copy(self, f: tuple[str | os.PathLike, Iterable[str | os.PathLike]]): + # it's a tuple with path to install to and a list of files + dir = convert_path(f[0]) + if not os.path.isabs(dir): + dir = os.path.join(self.install_dir, dir) + elif self.root: + dir = change_root(self.root, dir) + self.mkpath(dir) + + if f[1] == []: + # If there are no files listed, the user must be + # trying to create an empty directory, so add the + # directory to the list of output files. + self.outfiles.append(dir) + else: + # Copy files, adding them to the list of output files. + for data in f[1]: + data = convert_path(data) + (out, _) = self.copy_file(data, dir) self.outfiles.append(out) - else: - # it's a tuple with path to install to and a list of files - dir = convert_path(f[0]) - if not os.path.isabs(dir): - dir = os.path.join(self.install_dir, dir) - elif self.root: - dir = change_root(self.root, dir) - self.mkpath(dir) - - if f[1] == []: - # If there are no files listed, the user must be - # trying to create an empty directory, so add the - # directory to the list of output files. - self.outfiles.append(dir) - else: - # Copy files, adding them to the list of output files. - for data in f[1]: - data = convert_path(data) - (out, _) = self.copy_file(data, dir) - self.outfiles.append(out) + + @_copy.register(str) + @_copy.register(os.PathLike) + def _(self, f: str | os.PathLike): + # it's a simple file, so copy it + f = convert_path(f) + if self.warn_dir: + self.warn( + "setup script did not provide a directory for " + f"'{f}' -- installing right in '{self.install_dir}'" + ) + (out, _) = self.copy_file(f, self.install_dir) + self.outfiles.append(out) def get_inputs(self): return self.data_files or [] diff --git a/distutils/tests/test_install_data.py b/distutils/tests/test_install_data.py index f34070b1..4b15a269 100644 --- a/distutils/tests/test_install_data.py +++ b/distutils/tests/test_install_data.py @@ -1,11 +1,13 @@ """Tests for distutils.command.install_data.""" import os -from distutils.command.install_data import install_data -from distutils.tests import support +import pathlib import pytest +from distutils.command.install_data import install_data +from distutils.tests import support + @pytest.mark.usefixtures('save_env') class TestInstallData( @@ -18,22 +20,27 @@ def test_simple_run(self): # data_files can contain # - simple files + # - a Path object # - a tuple with a path, and a list of file one = os.path.join(pkg_dir, 'one') self.write_file(one, 'xxx') inst2 = os.path.join(pkg_dir, 'inst2') two = os.path.join(pkg_dir, 'two') self.write_file(two, 'xxx') + three = pathlib.Path(pkg_dir) / 'three' + self.write_file(three, 'xxx') - cmd.data_files = [one, (inst2, [two])] - assert cmd.get_inputs() == [one, (inst2, [two])] + cmd.data_files = [one, (inst2, [two]), three] + assert cmd.get_inputs() == [one, (inst2, [two]), three] # let's run the command cmd.ensure_finalized() cmd.run() # let's check the result - assert len(cmd.get_outputs()) == 2 + assert len(cmd.get_outputs()) == 3 + rthree = os.path.split(one)[-1] + assert os.path.exists(os.path.join(inst, rthree)) rtwo = os.path.split(two)[-1] assert os.path.exists(os.path.join(inst2, rtwo)) rone = os.path.split(one)[-1] @@ -46,21 +53,23 @@ def test_simple_run(self): cmd.run() # let's check the result - assert len(cmd.get_outputs()) == 2 + assert len(cmd.get_outputs()) == 3 + assert os.path.exists(os.path.join(inst, rthree)) assert os.path.exists(os.path.join(inst2, rtwo)) assert os.path.exists(os.path.join(inst, rone)) cmd.outfiles = [] # now using root and empty dir cmd.root = os.path.join(pkg_dir, 'root') - inst4 = os.path.join(pkg_dir, 'inst4') - three = os.path.join(cmd.install_dir, 'three') - self.write_file(three, 'xx') - cmd.data_files = [one, (inst2, [two]), ('inst3', [three]), (inst4, [])] + inst5 = os.path.join(pkg_dir, 'inst5') + four = os.path.join(cmd.install_dir, 'four') + self.write_file(four, 'xx') + cmd.data_files = [one, (inst2, [two]), three, ('inst5', [four]), (inst5, [])] cmd.ensure_finalized() cmd.run() # let's check the result - assert len(cmd.get_outputs()) == 4 + assert len(cmd.get_outputs()) == 5 + assert os.path.exists(os.path.join(inst, rthree)) assert os.path.exists(os.path.join(inst2, rtwo)) assert os.path.exists(os.path.join(inst, rone)) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 0de4e1a5..a614a1da 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -5,6 +5,7 @@ import email.policy import io import os +import pathlib import sys import sysconfig as stdlib_sysconfig import unittest.mock as mock @@ -72,6 +73,7 @@ def _join(path): os.path.join = _join assert convert_path('/home/to/my/stuff') == '/home/to/my/stuff' + assert convert_path(pathlib.Path('/home/to/my/stuff')) == '/home/to/my/stuff' # win os.sep = '\\' @@ -85,8 +87,11 @@ def _join(*path): convert_path('/home/to/my/stuff') with pytest.raises(ValueError): convert_path('home/to/my/stuff/') + with pytest.raises(ValueError): + convert_path(pathlib.Path('/home/to/my/stuff')) assert convert_path('home/to/my/stuff') == 'home\\to\\my\\stuff' + assert convert_path(pathlib.Path('home/to/my/stuff')) == 'home\\to\\my\\stuff' assert convert_path('.') == os.curdir def test_change_root(self): diff --git a/distutils/util.py b/distutils/util.py index 9db89b09..7751eb94 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -4,6 +4,8 @@ one of the other *util.py modules. """ +from __future__ import annotations + import functools import importlib.util import os @@ -116,7 +118,14 @@ def split_version(s): return [int(n) for n in s.split('.')] -def convert_path(pathname): +def convert_path(pathname: str | os.PathLike) -> str: + """ + Allow for pathlib.Path inputs and then make native. + """ + return make_native(os.fspath(pathname)) + + +def make_native(pathname: str) -> str: """Return 'pathname' as a name that will work on the native filesystem, i.e. split it on '/' and put it back together again using the current directory separator. Needed because filenames in the setup script are