Skip to content

Commit

Permalink
Merge pull request #272 from pypa/feature/pathlib-data-files
Browse files Browse the repository at this point in the history
Allow path objects for data-files
  • Loading branch information
jaraco committed Aug 2, 2024
2 parents dcb1bf8 + 8f2498a commit 9bebfda
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 90 deletions.
69 changes: 40 additions & 29 deletions distutils/command/install_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 []
Expand Down
31 changes: 20 additions & 11 deletions distutils/tests/test_install_data.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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]
Expand All @@ -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))
28 changes: 4 additions & 24 deletions distutils/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,30 +64,9 @@ def test_get_platform(self):
assert get_platform() == 'win-arm64'

def test_convert_path(self):
# linux/mac
os.sep = '/'

def _join(path):
return '/'.join(path)

os.path.join = _join

assert convert_path('/home/to/my/stuff') == '/home/to/my/stuff'

# win
os.sep = '\\'

def _join(*path):
return '\\'.join(path)

os.path.join = _join

with pytest.raises(ValueError):
convert_path('/home/to/my/stuff')
with pytest.raises(ValueError):
convert_path('home/to/my/stuff/')

assert convert_path('home/to/my/stuff') == 'home\\to\\my\\stuff'
expected = os.sep.join(('', 'home', 'to', 'my', 'stuff'))
assert convert_path('/home/to/my/stuff') == expected
assert convert_path(pathlib.Path('/home/to/my/stuff')) == expected
assert convert_path('.') == os.curdir

def test_change_root(self):
Expand Down
46 changes: 20 additions & 26 deletions distutils/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
one of the other *util.py modules.
"""

from __future__ import annotations

import functools
import importlib.util
import os
import pathlib
import re
import string
import subprocess
import sys
import sysconfig
import tempfile

from ._functools import pass_none
from ._log import log
from ._modified import newer
from .errors import DistutilsByteCompileError, DistutilsPlatformError
Expand Down Expand Up @@ -116,33 +120,23 @@ def split_version(s):
return [int(n) for n in s.split('.')]


def convert_path(pathname):
"""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
always supplied in Unix style, and have to be converted to the local
convention before we can actually use them in the filesystem. Raises
ValueError on non-Unix-ish systems if 'pathname' either starts or
ends with a slash.
@pass_none
def convert_path(pathname: str | os.PathLike) -> str:
r"""
Allow for pathlib.Path inputs, coax to a native path string.
If None is passed, will just pass it through as
Setuptools relies on this behavior.
>>> convert_path(None) is None
True
Removes empty paths.
>>> convert_path('foo/./bar').replace('\\', '/')
'foo/bar'
"""
if os.sep == '/':
return pathname
if not pathname:
return pathname
if pathname[0] == '/':
raise ValueError(f"path '{pathname}' cannot be absolute")
if pathname[-1] == '/':
raise ValueError(f"path '{pathname}' cannot end with '/'")

paths = pathname.split('/')
while '.' in paths:
paths.remove('.')
if not paths:
return os.curdir
return os.path.join(*paths)


# convert_path ()
return os.fspath(pathlib.PurePath(pathname))


def change_root(new_root, pathname):
Expand Down

0 comments on commit 9bebfda

Please sign in to comment.