Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/src/sage_setup/find.py
4052 views
1
# sage.doctest: needs SAGE_SRC
2
"""
3
Recursive Directory Contents
4
"""
5
# ****************************************************************************
6
# Copyright (C) 2014 Volker Braun <[email protected]>
7
# 2014 R. Andrew Ohana
8
# 2015-2018 Jeroen Demeyer
9
# 2017 Erik M. Bray
10
# 2021 Tobias Diez
11
# 2020-2022 Matthias Koeppe
12
#
13
# This program is free software: you can redistribute it and/or modify
14
# it under the terms of the GNU General Public License as published by
15
# the Free Software Foundation, either version 2 of the License, or
16
# (at your option) any later version.
17
# http://www.gnu.org/licenses/
18
# ****************************************************************************
19
20
import importlib.machinery
21
import importlib.util
22
23
import os
24
25
from collections import defaultdict
26
27
from sage.misc.package_dir import is_package_or_sage_namespace_package_dir as is_package_or_namespace_package_dir
28
from sage.misc.package_dir import read_distribution, SourceDistributionFilter
29
30
assert read_distribution # unused in this file, re-export for compatibility
31
32
33
def find_python_sources(src_dir, modules=['sage'], distributions=None,
34
exclude_distributions=None):
35
"""
36
Find all Python packages and Python/Cython modules in the sources.
37
38
INPUT:
39
40
- ``src_dir`` -- root directory for the sources
41
42
- ``modules`` -- (default: ``['sage']``) sequence of strings:
43
the top-level directories in ``src_dir`` to be considered
44
45
- ``distributions`` -- (default: ``None``) if not ``None``,
46
should be a sequence or set of strings: only find modules whose
47
``distribution`` (from a ``# sage_setup: distribution = PACKAGE``
48
directive in the module source file) is an element of
49
``distributions``.
50
51
- ``exclude_distributions`` -- (default: ``None``) if not ``None``,
52
should be a sequence or set of strings: exclude modules whose
53
``distribution`` (from a ``# sage_setup: distribution = PACKAGE``
54
directive in the module source file) is in ``exclude_distributions``.
55
56
OUTPUT:
57
58
Triple consisting of
59
60
- the list of package names (corresponding to ordinary packages
61
or namespace packages, according to
62
:func:`sage.misc.namespace_package.is_package_or_sage_namespace_package_dir`)
63
64
- Python module names (corresponding to ``*.py`` files in ordinary
65
or namespace packages)
66
67
- Cython extensions (corresponding to ``*.pyx`` files).
68
69
Both use dot as separator.
70
71
EXAMPLES::
72
73
sage: from sage.env import SAGE_SRC
74
sage: from sage_setup.find import find_python_sources
75
sage: py_packages, py_modules, cy_modules = find_python_sources(SAGE_SRC)
76
77
Ordinary package (with ``__init__.py``)::
78
79
sage: ['sage.structure' in L for L in (py_packages, py_modules)]
80
[True, False]
81
82
Python module in an ordinary package::
83
84
sage: ['sage.structure.formal_sum' in L for L in (py_packages, py_modules)]
85
[False, True]
86
87
Cython module in an ordinary package::
88
89
sage: ['sage.structure.sage_object' in L for L in (py_packages, py_modules)]
90
[False, False]
91
92
Subdirectory without any Python files::
93
94
sage: ['sage.doctest.tests' in L for L in (py_packages, py_modules)]
95
[False, False]
96
97
Another subdirectory that is neither an ordinary nor a namespace package::
98
99
sage: ['sage.extdata' in L for L in (py_packages, py_modules)]
100
[False, False]
101
102
Package designated to become an implicit namespace package (no ``__init__.py``, PEP 420,
103
but with an ``all.py`` file per Sage library conventions)::
104
105
sage: ['sage.graphs.graph_decompositions' in L for L in (py_packages, py_modules)]
106
[True, False]
107
108
Python module in a package designated to become an implicit namespace package::
109
110
sage: ['sage.graphs.graph_decompositions.modular_decomposition' in L for L in (py_packages, py_modules)]
111
[False, True]
112
113
Python file (not module) in a directory that is neither an ordinary nor a namespace package::
114
115
sage: ['sage.ext_data.nbconvert.postprocess' in L for L in (py_packages, py_modules)]
116
[False, False]
117
118
Filtering by distribution (distribution package)::
119
120
sage: find_python_sources(SAGE_SRC, distributions=['sagemath-tdlib'])
121
([], [], [<setuptools.extension.Extension('sage.graphs.graph_decompositions.tdlib')...>])
122
123
Benchmarking::
124
125
sage: timeit('find_python_sources(SAGE_SRC)', # random output
126
....: number=1, repeat=1)
127
1 loops, best of 1: 30 ms per loop
128
129
sage: timeit('find_python_sources(SAGE_SRC, distributions=[""])', # random output
130
....: number=1, repeat=1)
131
1 loops, best of 1: 850 ms per loop
132
133
sage: find_python_sources(SAGE_SRC, modules=['sage_setup'])
134
(['sage_setup', ...], [...'sage_setup.find'...], [])
135
"""
136
from setuptools import Extension
137
138
PYMOD_EXT = get_extensions('source')[0]
139
140
python_packages = []
141
python_modules = []
142
cython_modules = []
143
144
distribution_filter = SourceDistributionFilter(distributions, exclude_distributions)
145
146
cwd = os.getcwd()
147
try:
148
os.chdir(src_dir)
149
for module in modules:
150
for dirpath, dirnames, filenames in os.walk(module):
151
package = dirpath.replace(os.path.sep, '.')
152
if not is_package_or_namespace_package_dir(dirpath):
153
# Skip any subdirectories
154
dirnames[:] = []
155
continue
156
# Ordinary package or namespace package.
157
if distributions is None or '' in distributions:
158
python_packages.append(package)
159
160
for filename in filenames:
161
base, ext = os.path.splitext(filename)
162
filepath = os.path.join(dirpath, filename)
163
if ext == PYMOD_EXT and base != '__init__':
164
if filepath in distribution_filter:
165
python_modules.append(package + '.' + base)
166
if ext == '.pyx':
167
if filepath in distribution_filter:
168
cython_modules.append(Extension(package + '.' + base,
169
sources=[os.path.join(dirpath, filename)]))
170
171
finally:
172
os.chdir(cwd)
173
return python_packages, python_modules, cython_modules
174
175
176
def filter_cython_sources(src_dir, distributions, exclude_distributions=None):
177
"""
178
Find all Cython modules in the given source directory that belong to the
179
given distributions.
180
181
INPUT:
182
183
- ``src_dir`` -- root directory for the sources
184
185
- ``distributions`` -- a sequence or set of strings: only find modules whose
186
``distribution`` (from a ``# sage_setup: distribution = PACKAGE``
187
directive in the module source file) is an element of
188
``distributions``.
189
190
OUTPUT: list of absolute paths to Cython files (``*.pyx``)
191
192
EXAMPLES::
193
194
sage: from sage.env import SAGE_SRC
195
sage: from sage_setup.find import filter_cython_sources
196
sage: cython_modules = filter_cython_sources(SAGE_SRC, ["sagemath-tdlib"])
197
198
Cython module relying on tdlib::
199
200
sage: any(f.endswith('sage/graphs/graph_decompositions/tdlib.pyx') for f in cython_modules)
201
True
202
203
Cython module not relying on tdlib::
204
205
sage: any(f.endswith('sage/structure/sage_object.pyx') for f in cython_modules)
206
False
207
208
Benchmarking::
209
210
sage: timeit('filter_cython_sources(SAGE_SRC, ["sagemath-tdlib"])', # random output
211
....: number=1, repeat=1)
212
1 loops, best of 1: 850 ms per loop
213
"""
214
files: list[str] = []
215
distribution_filter = SourceDistributionFilter(distributions, exclude_distributions)
216
for dirpath, dirnames, filenames in os.walk(src_dir):
217
for filename in filenames:
218
filepath = os.path.join(dirpath, filename)
219
base, ext = os.path.splitext(filename)
220
if ext == '.pyx' and filepath in distribution_filter:
221
files.append(filepath)
222
223
return files
224
225
226
def _cythonized_dir(src_dir=None, editable_install=None):
227
"""
228
Return the path where Cython-generated files are placed by the build system.
229
230
INPUT:
231
232
- ``src_dir`` -- string or path (default: the value of ``SAGE_SRC``). The
233
root directory for the sources.
234
235
- ``editable_install`` -- boolean (default: determined from the existing
236
installation). Whether this is an editable install of the Sage library.
237
238
EXAMPLES::
239
240
sage: from sage_setup.find import _cythonized_dir
241
sage: from sage.env import SAGE_SRC
242
sage: _cythonized_dir(SAGE_SRC)
243
PosixPath('...')
244
sage: _cythonized_dir(SAGE_SRC, editable_install=False) # optional - sage_spkg
245
PosixPath('.../build/cythonized')
246
247
"""
248
from importlib import import_module
249
from pathlib import Path
250
from sage.env import SAGE_ROOT, SAGE_SRC
251
if editable_install is None:
252
if src_dir is None:
253
src_dir = SAGE_SRC
254
src_dir = Path(src_dir)
255
# sage.cpython is an ordinary package, so it has __file__
256
sage_cpython = import_module('sage.cpython')
257
d = Path(sage_cpython.__file__).resolve().parent.parent.parent
258
editable_install = d == src_dir.resolve()
259
if editable_install:
260
# Editable install: Cython generates files in the source tree
261
return src_dir
262
else:
263
return Path(SAGE_ROOT) / "build" / "pkgs" / "sagelib" / "src" / "build" / "cythonized"
264
265
266
def find_extra_files(src_dir, modules, cythonized_dir, special_filenames=[], *,
267
distributions=None):
268
"""
269
Find all extra files which should be installed.
270
271
These are (for each ``module`` in ``modules``):
272
273
1. From ``src_dir/module``: all .pyx, .pxd and .pxi files and files
274
listed in ``special_filenames``.
275
2. From ``cythonized_dir/module``: all .h files (both the .h files
276
from the sources, as well as all Cython-generated .h files).
277
278
The directories are searched recursively, but only package
279
directories (containing ``__init__.py`` or a Cython equivalent)
280
are considered.
281
282
INPUT:
283
284
- ``src_dir`` -- root directory for the sources
285
286
- ``modules`` -- sequence of strings:
287
the top-level directories in ``src_dir`` to be considered
288
289
- ``cythonized_dir`` -- the directory where the Cython-generated
290
files are
291
292
- ``special_filenames`` -- a list of filenames to be installed from
293
``src_dir``
294
295
- ``distributions`` -- (default: ``None``) if not ``None``,
296
should be a sequence or set of strings: only find files whose
297
``distribution`` (from a ``# sage_setup: distribution = PACKAGE``
298
directive in the file) is an element of ``distributions``.
299
300
OUTPUT: dict with items ``{dir: files}`` where ``dir`` is a
301
directory relative to ``src_dir`` and ``files`` is a list of
302
filenames inside that directory.
303
304
EXAMPLES::
305
306
sage: from sage_setup.find import find_extra_files, _cythonized_dir
307
sage: from sage.env import SAGE_SRC, SAGE_ROOT
308
sage: cythonized_dir = _cythonized_dir(SAGE_SRC)
309
sage: extras = find_extra_files(SAGE_SRC, ["sage"], cythonized_dir)
310
sage: extras["sage/libs/mpfr"]
311
[...sage/libs/mpfr/types.pxd...]
312
sage: sorted(extras["sage/ext/interpreters"])
313
['.../sage/ext/interpreters/wrapper_cdf.h', ...wrapper_cdf.pxd...]
314
"""
315
data_files = {}
316
cy_exts = ('.pxd', '.pxi', '.pyx')
317
318
cwd = os.getcwd()
319
try:
320
os.chdir(src_dir)
321
for module in modules:
322
for dir, dirnames, filenames in os.walk(module):
323
if not is_package_or_namespace_package_dir(dir):
324
continue
325
sdir = os.path.join(src_dir, dir)
326
cydir = os.path.join(cythonized_dir, dir)
327
328
files = [os.path.join(sdir, f) for f in filenames
329
if f.endswith(cy_exts) or f in special_filenames]
330
if os.path.isdir(cydir): # Not every directory contains Cython files
331
files += [os.path.join(cydir, f) for f in os.listdir(cydir)
332
if f.endswith(".h")]
333
else:
334
files += [os.path.join(sdir, f) for f in filenames
335
if f.endswith(".h")]
336
337
if distributions is not None:
338
files = [f for f in files
339
if read_distribution(f) in distributions]
340
341
if files:
342
data_files[dir] = files
343
finally:
344
os.chdir(cwd)
345
346
return data_files
347
348
349
def installed_files_by_module(site_packages, modules=('sage',)):
350
"""
351
Find all currently installed files
352
353
INPUT:
354
355
- ``site_packages`` -- string. The root Python path where the Sage
356
library is being installed. If the path doesn't exist, returns
357
an empty dictionary.
358
359
- ``modules`` -- list/tuple/iterable of strings (default:
360
``('sage',)``). The top-level directory name(s) in
361
``site_packages``.
362
363
OUTPUT:
364
365
A dictionary whose keys are module names (``'sage.module.foo'``)
366
and values are list of corresponding file names
367
``['sage/module/foo.py', 'sage/module/foo.pyc']`` relative to
368
``site_packages``.
369
370
EXAMPLES::
371
372
sage: site_packages = os.path.dirname(os.path.dirname(os.path.dirname(sage.cpython.__file__)))
373
sage: from sage_setup.find import installed_files_by_module
374
sage: files_by_module = installed_files_by_module(site_packages)
375
sage: (f,) = files_by_module['sage.structure.sage_object']; f
376
'sage/structure/sage_object...'
377
sage: (f1, f2) = sorted(files_by_module['sage.structure'])
378
sage: f1
379
'sage/structure/__init__.py'
380
sage: f2
381
'sage/structure/....pyc'
382
383
This takes about 30ms with warm cache::
384
385
sage: timeit('installed_files_by_module(site_packages)', # random output
386
....: number=1, repeat=1)
387
1 loops, best of 1: 29.6 ms per loop
388
"""
389
390
module_files = defaultdict(set)
391
module_exts = get_extensions()
392
393
def add(module, filename, dirpath):
394
# Find the longest extension that matches the filename
395
best_ext = ''
396
397
for ext in module_exts:
398
if filename.endswith(ext) and len(ext) > len(best_ext):
399
best_ext = ext
400
401
if not best_ext:
402
return
403
404
base = filename[:-len(best_ext)]
405
filename = os.path.join(dirpath, filename)
406
407
if base != '__init__':
408
module += '.' + base
409
410
module_files[module].add(filename)
411
412
cache_filename = importlib.util.cache_from_source(filename)
413
if os.path.exists(cache_filename):
414
module_files[module].add(cache_filename)
415
416
cwd = os.getcwd()
417
try:
418
os.chdir(site_packages)
419
except OSError:
420
return module_files
421
try:
422
for module in modules:
423
for dirpath, dirnames, filenames in os.walk(module):
424
module_dir = '.'.join(dirpath.split(os.path.sep))
425
426
if os.path.basename(dirpath) == '__pycache__':
427
continue
428
429
for filename in filenames:
430
add(module_dir, filename, dirpath)
431
finally:
432
os.chdir(cwd)
433
return module_files
434
435
436
def get_extensions(type=None):
437
"""
438
Returns the filename extensions for different types of Python module files.
439
440
By default returns all extensions, but can be filtered by type. The
441
possible types are 'source' (for pure Python sources), 'bytecode' (for
442
compiled bytecode files (i.e. pyc files), or 'extension' for C extension
443
modules.
444
445
INPUT:
446
447
- ``type`` -- the module type ('source', 'bytecode', or 'extension') or
448
None
449
450
EXAMPLES::
451
452
sage: from sage_setup.find import get_extensions
453
sage: get_extensions() # random - depends on Python version
454
['.so', 'module.so', '.py', '.pyc']
455
sage: get_extensions('source')
456
['.py']
457
sage: get_extensions('bytecode')
458
['.pyc']
459
sage: get_extensions('extension') # random - depends on Python version
460
['.so', 'module.so']
461
"""
462
463
if type:
464
type = type.lower()
465
if type not in ('source', 'bytecode', 'extension'):
466
raise ValueError(
467
"type must by one of 'source' (for Python sources), "
468
"'bytecode' (for compiled Python bytecoe), or 'extension' "
469
"(for C extension modules).")
470
471
# Note: There is at least one case, for extension modules, where the
472
# 'extension' does not begin with '.', but rather with 'module', for cases
473
# in Python's stdlib, for example, where an extension module can be named
474
# like "<modname>module.so". This breaks things for us if we have a Cython
475
# module literally named "module".
476
return [ext for ext in _get_extensions(type) if ext[0] == '.']
477
478
479
def _get_extensions(type):
480
"""
481
Python 3.3+ implementation of ``get_extensions()`` using the
482
`importlib.extensions` module.
483
"""
484
485
if type:
486
return {'source': importlib.machinery.SOURCE_SUFFIXES,
487
'bytecode': importlib.machinery.BYTECODE_SUFFIXES,
488
'extension': importlib.machinery.EXTENSION_SUFFIXES}[type]
489
490
return importlib.machinery.all_suffixes()
491
492