Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow path objects for data-files #272

Merged
merged 18 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d3c76bb
Allow path objects
InvincibleRMC Jul 19, 2024
12f2ef0
Need to include 'three' in the input.
jaraco Jul 19, 2024
8a9ca8b
Consolidate str and Path handling.
jaraco Jul 19, 2024
206ca2a
Expand convert_path to also accept pathlib.Path objects.
jaraco Jul 19, 2024
f7adff4
Prefer simply 'pathlib' for import.
jaraco Jul 19, 2024
b440f45
Extract a singledispatchmethod _copy for handling the copy of each da…
jaraco Jul 19, 2024
94b6d14
Use explicit registration for compatibility with older Pythons.
jaraco Jul 19, 2024
4d50db3
Prefer os.PathLike in convert_path
jaraco Jul 19, 2024
e88bd79
Convert needs to accept None for Setuptools' sake.
jaraco Jul 19, 2024
d48a881
In test_convert_path, utilize posixpath.join and ntpath.join for maxi…
jaraco Jul 19, 2024
8eb6b57
Wrap paths in PurePosixPath to ensure that any WindowsPaths don't get…
jaraco Jul 19, 2024
4232b01
convert_path no longer fails if passed a path with a trailing slash. …
jaraco Jul 19, 2024
b4df774
convert_path now converts to a platform-native path.Path, but then ca…
jaraco Jul 20, 2024
c67da35
Separate test_convert_path into two tests to avoid interactions in mo…
jaraco Jul 20, 2024
1e97e21
Remove expectation that a ValueError is raised for data_files being d…
jaraco Jul 20, 2024
4b3f16f
Simplify convert_path by simply relying on the logic in PurePath.
jaraco Jul 20, 2024
c4d3c3c
Harmonize convert_path tests across Unix and Windows.
jaraco Jul 20, 2024
a166815
Consolidate convert_path tests and just generate the expected value i…
jaraco Jul 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading