1 from functools
import partial
3 from distutils
.util
import convert_path
4 import distutils
.command
.build_py
as orig
9 import distutils
.errors
12 from pathlib
import Path
13 from typing
import Dict
, Iterable
, Iterator
, List
, Optional
, Tuple
15 from ..extern
.more_itertools
import unique_everseen
16 from ..warnings
import SetuptoolsDeprecationWarning
19 def make_writable(target
):
20 os
.chmod(target
, os
.stat(target
).st_mode | stat
.S_IWRITE
)
23 class build_py(orig
.build_py
):
24 """Enhanced 'build_py' command that includes data files with packages
26 The data files are specified via a 'package_data' argument to 'setup()'.
27 See 'setuptools.dist.Distribution' for more details.
29 Also, this version of the 'build_py' command allows you to specify both
30 '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
= []
45 self
, infile
, outfile
, preserve_mode
=1, preserve_times
=1, link
=None, level
=1
47 # Overwrite base class to allow using links
49 infile
= str(Path(infile
).resolve())
50 outfile
= str(Path(outfile
).resolve())
51 return super().copy_file(
52 infile
, outfile
, preserve_mode
, preserve_times
, link
, level
56 """Build modules, packages, and copy data files to build directory"""
57 if not (self
.py_modules
or self
.packages
) or self
.editable_mode
:
65 self
.build_package_data()
67 # Only compile actual .py files, using our base class' idea of what our
69 self
.byte_compile(orig
.build_py
.get_outputs(self
, include_bytecode
=0))
71 def __getattr__(self
, attr
):
72 "lazily compute data files"
73 if attr
== 'data_files':
74 self
.data_files
= self
._get
_data
_files
()
75 return self
.data_files
76 return orig
.build_py
.__getattr
__(self
, attr
)
78 def build_module(self
, module
, module_file
, package
):
79 outfile
, copied
= orig
.build_py
.build_module(self
, module
, module_file
, package
)
81 self
.__updated
_files
.append(outfile
)
82 return outfile
, copied
84 def _get_data_files(self
):
85 """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
86 self
.analyze_manifest()
87 return list(map(self
._get
_pkg
_data
_files
, self
.packages
or ()))
89 def get_data_files_without_manifest(self
):
91 Generate list of ``(package,src_dir,build_dir,filenames)`` tuples,
92 but without triggering any attempt to analyze or build the manifest.
94 # Prevent eventual errors from unset `manifest_files`
95 # (that would otherwise be set by `analyze_manifest`)
96 self
.__dict
__.setdefault('manifest_files', {})
97 return list(map(self
._get
_pkg
_data
_files
, self
.packages
or ()))
99 def _get_pkg_data_files(self
, package
):
100 # Locate package source directory
101 src_dir
= self
.get_package_dir(package
)
103 # Compute package build directory
104 build_dir
= os
.path
.join(*([self
.build_lib
] + package
.split('.')))
106 # Strip directory from globbed filenames
108 os
.path
.relpath(file, src_dir
)
109 for file in self
.find_data_files(package
, src_dir
)
111 return package
, src_dir
, build_dir
, filenames
113 def find_data_files(self
, package
, src_dir
):
114 """Return filenames for package's data files in 'src_dir'"""
115 patterns
= self
._get
_platform
_patterns
(
120 globs_expanded
= map(partial(glob
, recursive
=True), patterns
)
121 # flatten the expanded globs into an iterable of matches
122 globs_matches
= itertools
.chain
.from_iterable(globs_expanded
)
123 glob_files
= filter(os
.path
.isfile
, globs_matches
)
124 files
= itertools
.chain(
125 self
.manifest_files
.get(package
, []),
128 return self
.exclude_data_files(package
, src_dir
, files
)
130 def get_outputs(self
, include_bytecode
=1) -> List
[str]:
131 """See :class:`setuptools.commands.build.SubCommand`"""
132 if self
.editable_mode
:
133 return list(self
.get_output_mapping().keys())
134 return super().get_outputs(include_bytecode
)
136 def get_output_mapping(self
) -> Dict
[str, str]:
137 """See :class:`setuptools.commands.build.SubCommand`"""
138 mapping
= itertools
.chain(
139 self
._get
_package
_data
_output
_mapping
(),
140 self
._get
_module
_mapping
(),
142 return dict(sorted(mapping
, key
=lambda x
: x
[0]))
144 def _get_module_mapping(self
) -> Iterator
[Tuple
[str, str]]:
145 """Iterate over all modules producing (dest, src) pairs."""
146 for package
, module
, module_file
in self
.find_all_modules():
147 package
= package
.split('.')
148 filename
= self
.get_module_outfile(self
.build_lib
, package
, module
)
149 yield (filename
, module_file
)
151 def _get_package_data_output_mapping(self
) -> Iterator
[Tuple
[str, str]]:
152 """Iterate over package data producing (dest, src) pairs."""
153 for package
, src_dir
, build_dir
, filenames
in self
.data_files
:
154 for filename
in filenames
:
155 target
= os
.path
.join(build_dir
, filename
)
156 srcfile
= os
.path
.join(src_dir
, filename
)
157 yield (target
, srcfile
)
159 def build_package_data(self
):
160 """Copy data files into build directory"""
161 for target
, srcfile
in self
._get
_package
_data
_output
_mapping
():
162 self
.mkpath(os
.path
.dirname(target
))
163 _outf
, _copied
= self
.copy_file(srcfile
, target
)
164 make_writable(target
)
166 def analyze_manifest(self
):
167 self
.manifest_files
= mf
= {}
168 if not self
.distribution
.include_package_data
:
171 for package
in self
.packages
or ():
172 # Locate package source directory
173 src_dirs
[assert_relative(self
.get_package_dir(package
))] = package
176 getattr(self
, 'existing_egg_info_dir', None)
177 and Path(self
.existing_egg_info_dir
, "SOURCES.txt").exists()
179 egg_info_dir
= self
.existing_egg_info_dir
180 manifest
= Path(egg_info_dir
, "SOURCES.txt")
181 files
= manifest
.read_text(encoding
="utf-8").splitlines()
183 self
.run_command('egg_info')
184 ei_cmd
= self
.get_finalized_command('egg_info')
185 egg_info_dir
= ei_cmd
.egg_info
186 files
= ei_cmd
.filelist
.files
188 check
= _IncludePackageDataAbuse()
189 for path
in self
._filter
_build
_files
(files
, egg_info_dir
):
190 d
, f
= os
.path
.split(assert_relative(path
))
193 while d
and d
!= prev
and d
not in src_dirs
:
195 d
, df
= os
.path
.split(d
)
196 f
= os
.path
.join(df
, f
)
199 if check
.is_module(f
):
200 continue # it's a module, not data
202 importable
= check
.importable_subpackage(src_dirs
[d
], f
)
204 check
.warn(importable
)
205 mf
.setdefault(src_dirs
[d
], []).append(path
)
207 def _filter_build_files(self
, files
: Iterable
[str], egg_info
: str) -> Iterator
[str]:
209 ``build_meta`` may try to create egg_info outside of the project directory,
210 and this can be problematic for certain plugins (reported in issue #3500).
212 Extensions might also include between their sources files created on the
213 ``build_lib`` and ``build_temp`` directories.
215 This function should filter this case of invalid files out.
217 build
= self
.get_finalized_command("build")
218 build_dirs
= (egg_info
, self
.build_lib
, build
.build_temp
, build
.build_base
)
219 norm_dirs
= [os
.path
.normpath(p
) for p
in build_dirs
if p
]
222 norm_path
= os
.path
.normpath(file)
223 if not os
.path
.isabs(file) or all(d
not in norm_path
for d
in norm_dirs
):
226 def get_data_files(self
):
227 pass # Lazily compute data files in _get_data_files() function.
229 def check_package(self
, package
, package_dir
):
230 """Check namespace packages' __init__ for declare_namespace"""
232 return self
.packages_checked
[package
]
236 init_py
= orig
.build_py
.check_package(self
, package
, package_dir
)
237 self
.packages_checked
[package
] = init_py
239 if not init_py
or not self
.distribution
.namespace_packages
:
242 for pkg
in self
.distribution
.namespace_packages
:
243 if pkg
== package
or pkg
.startswith(package
+ '.'):
248 with io
.open(init_py
, 'rb') as f
:
250 if b
'declare_namespace' not in contents
:
251 raise distutils
.errors
.DistutilsError(
252 "Namespace package problem: %s is a namespace package, but "
253 "its\n__init__.py does not call declare_namespace()! Please "
254 'fix it.\n(See the setuptools manual under '
255 '"Namespace Packages" for details.)\n"' % (package
,)
259 def initialize_options(self
):
260 self
.packages_checked
= {}
261 orig
.build_py
.initialize_options(self
)
262 self
.editable_mode
= False
263 self
.existing_egg_info_dir
= None
265 def get_package_dir(self
, package
):
266 res
= orig
.build_py
.get_package_dir(self
, package
)
267 if self
.distribution
.src_root
is not None:
268 return os
.path
.join(self
.distribution
.src_root
, res
)
271 def exclude_data_files(self
, package
, src_dir
, files
):
272 """Filter filenames for package's data files in 'src_dir'"""
274 patterns
= self
._get
_platform
_patterns
(
275 self
.exclude_package_data
,
279 match_groups
= (fnmatch
.filter(files
, pattern
) for pattern
in patterns
)
280 # flatten the groups of matches into an iterable of matches
281 matches
= itertools
.chain
.from_iterable(match_groups
)
283 keepers
= (fn
for fn
in files
if fn
not in bad
)
285 return list(unique_everseen(keepers
))
288 def _get_platform_patterns(spec
, package
, src_dir
):
290 yield platform-specific path patterns (suitable for glob
291 or fn_match) from a glob-based spec (such as
292 self.package_data or self.exclude_package_data)
293 matching package in src_dir.
295 raw_patterns
= itertools
.chain(
297 spec
.get(package
, []),
300 # Each pattern has to be converted to a platform-specific path
301 os
.path
.join(src_dir
, convert_path(pattern
))
302 for pattern
in raw_patterns
306 def assert_relative(path
):
307 if not os
.path
.isabs(path
):
309 from distutils
.errors
import DistutilsSetupError
314 Error: setup script specifies an absolute path:
318 setup() arguments must *always* be /-separated paths relative to the
319 setup.py directory, *never* absolute paths.
324 raise DistutilsSetupError(msg
)
327 class _IncludePackageDataAbuse
:
328 """Inform users that package or module is included as 'data file'"""
330 class _Warning(SetuptoolsDeprecationWarning
):
332 Package {importable!r} is absent from the `packages` configuration.
336 ############################
337 # Package would be ignored #
338 ############################
339 Python recognizes {importable!r} as an importable package[^1],
340 but it is absent from setuptools' `packages` configuration.
342 This leads to an ambiguous overall configuration. If you want to distribute this
343 package, please make sure that {importable!r} is explicitly added
344 to the `packages` configuration field.
346 Alternatively, you can also rely on setuptools' discovery methods
347 (for example by using `find_namespace_packages(...)`/`find_namespace:`
348 instead of `find_packages(...)`/`find:`).
350 You can read more about "package discovery" on setuptools documentation page:
352 - https://setuptools.pypa.io/en/latest/userguide/package_discovery.html
354 If you don't want {importable!r} to be distributed and are
355 already explicitly excluding {importable!r} via
356 `find_namespace_packages(...)/find_namespace` or `find_packages(...)/find`,
357 you can try to use `exclude_package_data`, or `include-package-data=False` in
358 combination with a more fine grained `package-data` configuration.
360 You can read more about "package data files" on setuptools documentation page:
362 - https://setuptools.pypa.io/en/latest/userguide/datafiles.html
365 [^1]: For Python, any directory (with suitable naming) can be imported,
366 even if it does not contain any `.py` files.
367 On the other hand, currently there is no concept of package data
368 directory, all directories are treated like packages.
370 # _DUE_DATE: still not defined as this is particularly controversial.
371 # Warning initially introduced in May 2022. See issue #3340 for discussion.
374 self
._already
_warned
= set()
376 def is_module(self
, file):
377 return file.endswith(".py") and file[: -len(".py")].isidentifier()
379 def importable_subpackage(self
, parent
, file):
380 pkg
= Path(file).parent
381 parts
= list(itertools
.takewhile(str.isidentifier
, pkg
.parts
))
383 return ".".join([parent
, *parts
])
386 def warn(self
, importable
):
387 if importable
not in self
._already
_warned
:
388 self
._Warning
.emit(importable
=importable
)
389 self
._already
_warned
.add(importable
)