1 from functools
import partial
3 from distutils
.util
import convert_path
4 import distutils
.command
.build_py
as orig
9 import distutils
.errors
13 from pathlib
import Path
14 from typing
import Dict
, Iterable
, Iterator
, List
, Optional
, Tuple
16 from setuptools
._deprecation
_warning
import SetuptoolsDeprecationWarning
17 from setuptools
.extern
.more_itertools
import unique_everseen
20 def make_writable(target
):
21 os
.chmod(target
, os
.stat(target
).st_mode | stat
.S_IWRITE
)
24 class build_py(orig
.build_py
):
25 """Enhanced 'build_py' command that includes data files with packages
27 The data files are specified via a 'package_data' argument to 'setup()'.
28 See 'setuptools.dist.Distribution' for more details.
30 Also, this version of the 'build_py' command allows you to specify both
31 'py_modules' and 'packages' in the same setup operation.
33 editable_mode
: bool = False
34 existing_egg_info_dir
: Optional
[str] = None #: Private API, internal use only.
36 def finalize_options(self
):
37 orig
.build_py
.finalize_options(self
)
38 self
.package_data
= self
.distribution
.package_data
39 self
.exclude_package_data
= self
.distribution
.exclude_package_data
or {}
40 if 'data_files' in self
.__dict
__:
41 del self
.__dict
__['data_files']
42 self
.__updated
_files
= []
44 def copy_file(self
, infile
, outfile
, preserve_mode
=1, preserve_times
=1,
46 # Overwrite base class to allow using links
48 infile
= str(Path(infile
).resolve())
49 outfile
= str(Path(outfile
).resolve())
50 return super().copy_file(infile
, outfile
, preserve_mode
, preserve_times
,
54 """Build modules, packages, and copy data files to build directory"""
55 if not (self
.py_modules
or self
.packages
) or self
.editable_mode
:
63 self
.build_package_data()
65 # Only compile actual .py files, using our base class' idea of what our
67 self
.byte_compile(orig
.build_py
.get_outputs(self
, include_bytecode
=0))
69 def __getattr__(self
, attr
):
70 "lazily compute data files"
71 if attr
== 'data_files':
72 self
.data_files
= self
._get
_data
_files
()
73 return self
.data_files
74 return orig
.build_py
.__getattr
__(self
, attr
)
76 def build_module(self
, module
, module_file
, package
):
77 outfile
, copied
= orig
.build_py
.build_module(self
, module
, module_file
, package
)
79 self
.__updated
_files
.append(outfile
)
80 return outfile
, copied
82 def _get_data_files(self
):
83 """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
84 self
.analyze_manifest()
85 return list(map(self
._get
_pkg
_data
_files
, self
.packages
or ()))
87 def get_data_files_without_manifest(self
):
89 Generate list of ``(package,src_dir,build_dir,filenames)`` tuples,
90 but without triggering any attempt to analyze or build the manifest.
92 # Prevent eventual errors from unset `manifest_files`
93 # (that would otherwise be set by `analyze_manifest`)
94 self
.__dict
__.setdefault('manifest_files', {})
95 return list(map(self
._get
_pkg
_data
_files
, self
.packages
or ()))
97 def _get_pkg_data_files(self
, package
):
98 # Locate package source directory
99 src_dir
= self
.get_package_dir(package
)
101 # Compute package build directory
102 build_dir
= os
.path
.join(*([self
.build_lib
] + package
.split('.')))
104 # Strip directory from globbed filenames
106 os
.path
.relpath(file, src_dir
)
107 for file in self
.find_data_files(package
, src_dir
)
109 return package
, src_dir
, build_dir
, filenames
111 def find_data_files(self
, package
, src_dir
):
112 """Return filenames for package's data files in 'src_dir'"""
113 patterns
= self
._get
_platform
_patterns
(
118 globs_expanded
= map(partial(glob
, recursive
=True), patterns
)
119 # flatten the expanded globs into an iterable of matches
120 globs_matches
= itertools
.chain
.from_iterable(globs_expanded
)
121 glob_files
= filter(os
.path
.isfile
, globs_matches
)
122 files
= itertools
.chain(
123 self
.manifest_files
.get(package
, []),
126 return self
.exclude_data_files(package
, src_dir
, files
)
128 def get_outputs(self
, include_bytecode
=1) -> List
[str]:
129 """See :class:`setuptools.commands.build.SubCommand`"""
130 if self
.editable_mode
:
131 return list(self
.get_output_mapping().keys())
132 return super().get_outputs(include_bytecode
)
134 def get_output_mapping(self
) -> Dict
[str, str]:
135 """See :class:`setuptools.commands.build.SubCommand`"""
136 mapping
= itertools
.chain(
137 self
._get
_package
_data
_output
_mapping
(),
138 self
._get
_module
_mapping
(),
140 return dict(sorted(mapping
, key
=lambda x
: x
[0]))
142 def _get_module_mapping(self
) -> Iterator
[Tuple
[str, str]]:
143 """Iterate over all modules producing (dest, src) pairs."""
144 for (package
, module
, module_file
) in self
.find_all_modules():
145 package
= package
.split('.')
146 filename
= self
.get_module_outfile(self
.build_lib
, package
, module
)
147 yield (filename
, module_file
)
149 def _get_package_data_output_mapping(self
) -> Iterator
[Tuple
[str, str]]:
150 """Iterate over package data producing (dest, src) pairs."""
151 for package
, src_dir
, build_dir
, filenames
in self
.data_files
:
152 for filename
in filenames
:
153 target
= os
.path
.join(build_dir
, filename
)
154 srcfile
= os
.path
.join(src_dir
, filename
)
155 yield (target
, srcfile
)
157 def build_package_data(self
):
158 """Copy data files into build directory"""
159 for target
, srcfile
in self
._get
_package
_data
_output
_mapping
():
160 self
.mkpath(os
.path
.dirname(target
))
161 _outf
, _copied
= self
.copy_file(srcfile
, target
)
162 make_writable(target
)
164 def analyze_manifest(self
):
165 self
.manifest_files
= mf
= {}
166 if not self
.distribution
.include_package_data
:
169 for package
in self
.packages
or ():
170 # Locate package source directory
171 src_dirs
[assert_relative(self
.get_package_dir(package
))] = package
174 getattr(self
, 'existing_egg_info_dir', None)
175 and Path(self
.existing_egg_info_dir
, "SOURCES.txt").exists()
177 egg_info_dir
= self
.existing_egg_info_dir
178 manifest
= Path(egg_info_dir
, "SOURCES.txt")
179 files
= manifest
.read_text(encoding
="utf-8").splitlines()
181 self
.run_command('egg_info')
182 ei_cmd
= self
.get_finalized_command('egg_info')
183 egg_info_dir
= ei_cmd
.egg_info
184 files
= ei_cmd
.filelist
.files
186 check
= _IncludePackageDataAbuse()
187 for path
in self
._filter
_build
_files
(files
, egg_info_dir
):
188 d
, f
= os
.path
.split(assert_relative(path
))
191 while d
and d
!= prev
and d
not in src_dirs
:
193 d
, df
= os
.path
.split(d
)
194 f
= os
.path
.join(df
, f
)
197 if check
.is_module(f
):
198 continue # it's a module, not data
200 importable
= check
.importable_subpackage(src_dirs
[d
], f
)
202 check
.warn(importable
)
203 mf
.setdefault(src_dirs
[d
], []).append(path
)
205 def _filter_build_files(self
, files
: Iterable
[str], egg_info
: str) -> Iterator
[str]:
207 ``build_meta`` may try to create egg_info outside of the project directory,
208 and this can be problematic for certain plugins (reported in issue #3500).
210 Extensions might also include between their sources files created on the
211 ``build_lib`` and ``build_temp`` directories.
213 This function should filter this case of invalid files out.
215 build
= self
.get_finalized_command("build")
216 build_dirs
= (egg_info
, self
.build_lib
, build
.build_temp
, build
.build_base
)
217 norm_dirs
= [os
.path
.normpath(p
) for p
in build_dirs
if p
]
220 norm_path
= os
.path
.normpath(file)
221 if not os
.path
.isabs(file) or all(d
not in norm_path
for d
in norm_dirs
):
224 def get_data_files(self
):
225 pass # Lazily compute data files in _get_data_files() function.
227 def check_package(self
, package
, package_dir
):
228 """Check namespace packages' __init__ for declare_namespace"""
230 return self
.packages_checked
[package
]
234 init_py
= orig
.build_py
.check_package(self
, package
, package_dir
)
235 self
.packages_checked
[package
] = init_py
237 if not init_py
or not self
.distribution
.namespace_packages
:
240 for pkg
in self
.distribution
.namespace_packages
:
241 if pkg
== package
or pkg
.startswith(package
+ '.'):
246 with io
.open(init_py
, 'rb') as f
:
248 if b
'declare_namespace' not in contents
:
249 raise distutils
.errors
.DistutilsError(
250 "Namespace package problem: %s is a namespace package, but "
251 "its\n__init__.py does not call declare_namespace()! Please "
252 'fix it.\n(See the setuptools manual under '
253 '"Namespace Packages" for details.)\n"' % (package
,)
257 def initialize_options(self
):
258 self
.packages_checked
= {}
259 orig
.build_py
.initialize_options(self
)
260 self
.editable_mode
= False
261 self
.existing_egg_info_dir
= None
263 def get_package_dir(self
, package
):
264 res
= orig
.build_py
.get_package_dir(self
, package
)
265 if self
.distribution
.src_root
is not None:
266 return os
.path
.join(self
.distribution
.src_root
, res
)
269 def exclude_data_files(self
, package
, src_dir
, files
):
270 """Filter filenames for package's data files in 'src_dir'"""
272 patterns
= self
._get
_platform
_patterns
(
273 self
.exclude_package_data
,
277 match_groups
= (fnmatch
.filter(files
, pattern
) for pattern
in patterns
)
278 # flatten the groups of matches into an iterable of matches
279 matches
= itertools
.chain
.from_iterable(match_groups
)
281 keepers
= (fn
for fn
in files
if fn
not in bad
)
283 return list(unique_everseen(keepers
))
286 def _get_platform_patterns(spec
, package
, src_dir
):
288 yield platform-specific path patterns (suitable for glob
289 or fn_match) from a glob-based spec (such as
290 self.package_data or self.exclude_package_data)
291 matching package in src_dir.
293 raw_patterns
= itertools
.chain(
295 spec
.get(package
, []),
298 # Each pattern has to be converted to a platform-specific path
299 os
.path
.join(src_dir
, convert_path(pattern
))
300 for pattern
in raw_patterns
304 def assert_relative(path
):
305 if not os
.path
.isabs(path
):
307 from distutils
.errors
import DistutilsSetupError
312 Error: setup script specifies an absolute path:
316 setup() arguments must *always* be /-separated paths relative to the
317 setup.py directory, *never* absolute paths.
322 raise DistutilsSetupError(msg
)
325 class _IncludePackageDataAbuse
:
326 """Inform users that package or module is included as 'data file'"""
329 Installing {importable!r} as data is deprecated, please list it in `packages`.
331 ############################
332 # Package would be ignored #
333 ############################
334 Python recognizes {importable!r} as an importable package,
335 but it is not listed in the `packages` configuration of setuptools.
337 {importable!r} has been automatically added to the distribution only
338 because it may contain data files, but this behavior is likely to change
339 in future versions of setuptools (and therefore is considered deprecated).
341 Please make sure that {importable!r} is included as a package by using
342 the `packages` configuration field or the proper discovery methods
343 (for example by using `find_namespace_packages(...)`/`find_namespace:`
344 instead of `find_packages(...)`/`find:`).
346 You can read more about "package discovery" and "data files" on setuptools
352 self
._already
_warned
= set()
354 def is_module(self
, file):
355 return file.endswith(".py") and file[:-len(".py")].isidentifier()
357 def importable_subpackage(self
, parent
, file):
358 pkg
= Path(file).parent
359 parts
= list(itertools
.takewhile(str.isidentifier
, pkg
.parts
))
361 return ".".join([parent
, *parts
])
364 def warn(self
, importable
):
365 if importable
not in self
._already
_warned
:
366 msg
= textwrap
.dedent(self
.MESSAGE
).format(importable
=importable
)
367 warnings
.warn(msg
, SetuptoolsDeprecationWarning
, stacklevel
=2)
368 self
._already
_warned
.add(importable
)