Skip to content

Commit 9bebfda

Browse files
committed
Merge pull request pypa/distutils#272 from pypa/feature/pathlib-data-files
Allow path objects for data-files
2 parents dcb1bf8 + 8f2498a commit 9bebfda

File tree

4 files changed

+84
-90
lines changed

4 files changed

+84
-90
lines changed

distutils/command/install_data.py

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55

66
# contributed by Bastian Kleineidam
77

8+
from __future__ import annotations
9+
10+
import functools
811
import os
912

13+
from typing import Iterable
14+
1015
from ..core import Command
1116
from ..util import change_root, convert_path
1217

@@ -46,36 +51,42 @@ def finalize_options(self):
4651
def run(self):
4752
self.mkpath(self.install_dir)
4853
for f in self.data_files:
49-
if isinstance(f, str):
50-
# it's a simple file, so copy it
51-
f = convert_path(f)
52-
if self.warn_dir:
53-
self.warn(
54-
"setup script did not provide a directory for "
55-
f"'{f}' -- installing right in '{self.install_dir}'"
56-
)
57-
(out, _) = self.copy_file(f, self.install_dir)
54+
self._copy(f)
55+
56+
@functools.singledispatchmethod
57+
def _copy(self, f: tuple[str | os.PathLike, Iterable[str | os.PathLike]]):
58+
# it's a tuple with path to install to and a list of files
59+
dir = convert_path(f[0])
60+
if not os.path.isabs(dir):
61+
dir = os.path.join(self.install_dir, dir)
62+
elif self.root:
63+
dir = change_root(self.root, dir)
64+
self.mkpath(dir)
65+
66+
if f[1] == []:
67+
# If there are no files listed, the user must be
68+
# trying to create an empty directory, so add the
69+
# directory to the list of output files.
70+
self.outfiles.append(dir)
71+
else:
72+
# Copy files, adding them to the list of output files.
73+
for data in f[1]:
74+
data = convert_path(data)
75+
(out, _) = self.copy_file(data, dir)
5876
self.outfiles.append(out)
59-
else:
60-
# it's a tuple with path to install to and a list of files
61-
dir = convert_path(f[0])
62-
if not os.path.isabs(dir):
63-
dir = os.path.join(self.install_dir, dir)
64-
elif self.root:
65-
dir = change_root(self.root, dir)
66-
self.mkpath(dir)
67-
68-
if f[1] == []:
69-
# If there are no files listed, the user must be
70-
# trying to create an empty directory, so add the
71-
# directory to the list of output files.
72-
self.outfiles.append(dir)
73-
else:
74-
# Copy files, adding them to the list of output files.
75-
for data in f[1]:
76-
data = convert_path(data)
77-
(out, _) = self.copy_file(data, dir)
78-
self.outfiles.append(out)
77+
78+
@_copy.register(str)
79+
@_copy.register(os.PathLike)
80+
def _(self, f: str | os.PathLike):
81+
# it's a simple file, so copy it
82+
f = convert_path(f)
83+
if self.warn_dir:
84+
self.warn(
85+
"setup script did not provide a directory for "
86+
f"'{f}' -- installing right in '{self.install_dir}'"
87+
)
88+
(out, _) = self.copy_file(f, self.install_dir)
89+
self.outfiles.append(out)
7990

8091
def get_inputs(self):
8192
return self.data_files or []

distutils/tests/test_install_data.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Tests for distutils.command.install_data."""
22

33
import os
4-
from distutils.command.install_data import install_data
5-
from distutils.tests import support
4+
import pathlib
65

76
import pytest
87

8+
from distutils.command.install_data import install_data
9+
from distutils.tests import support
10+
911

1012
@pytest.mark.usefixtures('save_env')
1113
class TestInstallData(
@@ -18,22 +20,27 @@ def test_simple_run(self):
1820

1921
# data_files can contain
2022
# - simple files
23+
# - a Path object
2124
# - a tuple with a path, and a list of file
2225
one = os.path.join(pkg_dir, 'one')
2326
self.write_file(one, 'xxx')
2427
inst2 = os.path.join(pkg_dir, 'inst2')
2528
two = os.path.join(pkg_dir, 'two')
2629
self.write_file(two, 'xxx')
30+
three = pathlib.Path(pkg_dir) / 'three'
31+
self.write_file(three, 'xxx')
2732

28-
cmd.data_files = [one, (inst2, [two])]
29-
assert cmd.get_inputs() == [one, (inst2, [two])]
33+
cmd.data_files = [one, (inst2, [two]), three]
34+
assert cmd.get_inputs() == [one, (inst2, [two]), three]
3035

3136
# let's run the command
3237
cmd.ensure_finalized()
3338
cmd.run()
3439

3540
# let's check the result
36-
assert len(cmd.get_outputs()) == 2
41+
assert len(cmd.get_outputs()) == 3
42+
rthree = os.path.split(one)[-1]
43+
assert os.path.exists(os.path.join(inst, rthree))
3744
rtwo = os.path.split(two)[-1]
3845
assert os.path.exists(os.path.join(inst2, rtwo))
3946
rone = os.path.split(one)[-1]
@@ -46,21 +53,23 @@ def test_simple_run(self):
4653
cmd.run()
4754

4855
# let's check the result
49-
assert len(cmd.get_outputs()) == 2
56+
assert len(cmd.get_outputs()) == 3
57+
assert os.path.exists(os.path.join(inst, rthree))
5058
assert os.path.exists(os.path.join(inst2, rtwo))
5159
assert os.path.exists(os.path.join(inst, rone))
5260
cmd.outfiles = []
5361

5462
# now using root and empty dir
5563
cmd.root = os.path.join(pkg_dir, 'root')
56-
inst4 = os.path.join(pkg_dir, 'inst4')
57-
three = os.path.join(cmd.install_dir, 'three')
58-
self.write_file(three, 'xx')
59-
cmd.data_files = [one, (inst2, [two]), ('inst3', [three]), (inst4, [])]
64+
inst5 = os.path.join(pkg_dir, 'inst5')
65+
four = os.path.join(cmd.install_dir, 'four')
66+
self.write_file(four, 'xx')
67+
cmd.data_files = [one, (inst2, [two]), three, ('inst5', [four]), (inst5, [])]
6068
cmd.ensure_finalized()
6169
cmd.run()
6270

6371
# let's check the result
64-
assert len(cmd.get_outputs()) == 4
72+
assert len(cmd.get_outputs()) == 5
73+
assert os.path.exists(os.path.join(inst, rthree))
6574
assert os.path.exists(os.path.join(inst2, rtwo))
6675
assert os.path.exists(os.path.join(inst, rone))

distutils/tests/test_util.py

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import email.policy
66
import io
77
import os
8+
import pathlib
89
import sys
910
import sysconfig as stdlib_sysconfig
1011
import unittest.mock as mock
@@ -63,30 +64,9 @@ def test_get_platform(self):
6364
assert get_platform() == 'win-arm64'
6465

6566
def test_convert_path(self):
66-
# linux/mac
67-
os.sep = '/'
68-
69-
def _join(path):
70-
return '/'.join(path)
71-
72-
os.path.join = _join
73-
74-
assert convert_path('/home/to/my/stuff') == '/home/to/my/stuff'
75-
76-
# win
77-
os.sep = '\\'
78-
79-
def _join(*path):
80-
return '\\'.join(path)
81-
82-
os.path.join = _join
83-
84-
with pytest.raises(ValueError):
85-
convert_path('/home/to/my/stuff')
86-
with pytest.raises(ValueError):
87-
convert_path('home/to/my/stuff/')
88-
89-
assert convert_path('home/to/my/stuff') == 'home\\to\\my\\stuff'
67+
expected = os.sep.join(('', 'home', 'to', 'my', 'stuff'))
68+
assert convert_path('/home/to/my/stuff') == expected
69+
assert convert_path(pathlib.Path('/home/to/my/stuff')) == expected
9070
assert convert_path('.') == os.curdir
9171

9272
def test_change_root(self):

distutils/util.py

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
one of the other *util.py modules.
55
"""
66

7+
from __future__ import annotations
8+
79
import functools
810
import importlib.util
911
import os
12+
import pathlib
1013
import re
1114
import string
1215
import subprocess
1316
import sys
1417
import sysconfig
1518
import tempfile
1619

20+
from ._functools import pass_none
1721
from ._log import log
1822
from ._modified import newer
1923
from .errors import DistutilsByteCompileError, DistutilsPlatformError
@@ -116,33 +120,23 @@ def split_version(s):
116120
return [int(n) for n in s.split('.')]
117121

118122

119-
def convert_path(pathname):
120-
"""Return 'pathname' as a name that will work on the native filesystem,
121-
i.e. split it on '/' and put it back together again using the current
122-
directory separator. Needed because filenames in the setup script are
123-
always supplied in Unix style, and have to be converted to the local
124-
convention before we can actually use them in the filesystem. Raises
125-
ValueError on non-Unix-ish systems if 'pathname' either starts or
126-
ends with a slash.
123+
@pass_none
124+
def convert_path(pathname: str | os.PathLike) -> str:
125+
r"""
126+
Allow for pathlib.Path inputs, coax to a native path string.
127+
128+
If None is passed, will just pass it through as
129+
Setuptools relies on this behavior.
130+
131+
>>> convert_path(None) is None
132+
True
133+
134+
Removes empty paths.
135+
136+
>>> convert_path('foo/./bar').replace('\\', '/')
137+
'foo/bar'
127138
"""
128-
if os.sep == '/':
129-
return pathname
130-
if not pathname:
131-
return pathname
132-
if pathname[0] == '/':
133-
raise ValueError(f"path '{pathname}' cannot be absolute")
134-
if pathname[-1] == '/':
135-
raise ValueError(f"path '{pathname}' cannot end with '/'")
136-
137-
paths = pathname.split('/')
138-
while '.' in paths:
139-
paths.remove('.')
140-
if not paths:
141-
return os.curdir
142-
return os.path.join(*paths)
143-
144-
145-
# convert_path ()
139+
return os.fspath(pathlib.PurePath(pathname))
146140

147141

148142
def change_root(new_root, pathname):

0 commit comments

Comments
 (0)