2
Cython -- Things that don't belong anywhere else in particular
9
os=object, sys=object, re=object, io=object, glob=object, shutil=object, tempfile=object,
10
update_wrapper=object, partial=object, wraps=object, cython_version=object,
11
_cache_function=object, _function_caches=list, _parse_file_version=object, _match_file_encoding=object,
24
if sys.version_info < (3, 9):
32
from functools import update_wrapper, partial
34
def _update_wrapper(wrapper, wrapped):
36
return update_wrapper(wrapper, wrapped)
37
except AttributeError:
41
return partial(_update_wrapper, wrapped=wrapped)
43
from functools import wraps
46
from . import __version__ as cython_version
48
PACKAGE_FILES = ("__init__.py", "__init__.pyc", "__init__.pyx", "__init__.pxd")
50
_build_cache_name = "__{}_cache".format
51
_CACHE_NAME_PATTERN = re.compile(r"^__(.+)_cache$")
53
modification_time = os.path.getmtime
55
GENERATED_BY_MARKER = "/* Generated by Cython %s */" % cython_version
56
GENERATED_BY_MARKER_BYTES = GENERATED_BY_MARKER.encode('us-ascii')
59
class _TryFinallyGeneratorContextManager:
61
Fast, bare minimum @contextmanager, only for try-finally, not for exception handling.
63
def __init__(self, gen):
67
return next(self._gen)
69
def __exit__(self, exc_type, exc_val, exc_tb):
72
except (StopIteration, GeneratorExit):
76
def try_finally_contextmanager(gen_func):
78
def make_gen(*args, **kwargs):
79
return _TryFinallyGeneratorContextManager(gen_func(*args, **kwargs))
84
from functools import cache as _cache_function
86
from functools import lru_cache
87
_cache_function = lru_cache(maxsize=None)
93
def clear_function_caches():
94
for cache in _function_caches:
98
def cached_function(f):
99
cf = _cache_function(f)
100
_function_caches.append(cf)
106
def _find_cache_attributes(obj):
107
"""The function iterates over the attributes of the object and,
108
if it finds the name of the cache, it returns it and the corresponding method name.
109
The method may not be present in the object.
111
for attr_name in dir(obj):
112
match = _CACHE_NAME_PATTERN.match(attr_name)
113
if match is not None:
114
yield attr_name, match.group(1)
117
def clear_method_caches(obj):
118
"""Removes every cache found in the object,
119
if a corresponding method exists for that cache.
121
for cache_name, method_name in _find_cache_attributes(obj):
122
if hasattr(obj, method_name):
123
delattr(obj, cache_name)
129
cache_name = _build_cache_name(f.__name__)
131
def wrapper(self, *args):
132
cache = getattr(self, cache_name, None)
135
setattr(self, cache_name, cache)
138
res = cache[args] = f(self, *args)
144
def replace_suffix(path, newsuf):
145
base, _ = os.path.splitext(path)
149
def open_new_file(path):
150
if os.path.exists(path):
157
return open(path, "w", encoding="UTF-8")
160
def castrate_file(path, st):
165
if not is_cython_generated_file(path, allow_failed=True, if_not_found=False):
169
f = open_new_file(path)
174
"#error Do not use this file, it is the result of a failed Cython compilation.\n")
177
os.utime(path, (st.st_atime, st.st_mtime-1))
180
def is_cython_generated_file(path, allow_failed=False, if_not_found=True):
181
failure_marker = b"#error Do not use this file, it is the result of a failed Cython compilation."
183
if os.path.exists(path):
185
with open(path, "rb") as f:
186
file_content = f.read(len(failure_marker))
190
if file_content is None:
196
file_content.startswith(b"/* Generated by Cython ") or
198
(allow_failed and file_content == failure_marker) or
204
def file_generated_by_this_cython(path):
206
if os.path.exists(path):
208
with open(path, "rb") as f:
209
file_content = f.read(len(GENERATED_BY_MARKER_BYTES))
212
return file_content and file_content.startswith(GENERATED_BY_MARKER_BYTES)
215
def file_newer_than(path, time):
216
ftime = modification_time(path)
220
def safe_makedirs(path):
224
if not os.path.isdir(path):
228
def copy_file_to_dir_if_newer(sourcefile, destdir):
230
Copy file sourcefile to directory destdir (creating it if needed),
231
preserving metadata. If the destination file exists and is not
232
older than the source file, the copying is skipped.
234
destfile = os.path.join(destdir, os.path.basename(sourcefile))
236
desttime = modification_time(destfile)
239
safe_makedirs(destdir)
242
if not file_newer_than(sourcefile, desttime):
244
shutil.copy2(sourcefile, destfile)
248
def find_root_package_dir(file_path):
249
dir = os.path.dirname(file_path)
252
elif is_package_dir(dir):
253
return find_root_package_dir(dir)
259
def check_package_dir(dir_path, package_names):
261
for dirname in package_names:
262
dir_path = os.path.join(dir_path, dirname)
263
has_init = contains_init(dir_path)
266
return dir_path, namespace
270
def contains_init(dir_path):
271
for filename in PACKAGE_FILES:
272
path = os.path.join(dir_path, filename)
273
if path_exists(path):
277
def is_package_dir(dir_path):
278
if contains_init(dir_path):
283
def path_exists(path):
285
if os.path.exists(path):
292
archive_path = getattr(loader, 'archive', None)
294
normpath = os.path.normpath(path)
295
if normpath.startswith(archive_path):
296
arcname = normpath[len(archive_path)+1:]
298
loader.get_data(arcname)
307
_parse_file_version = re.compile(r".*[.]cython-([0-9]+)[.][^./\\]+$").findall
311
def find_versioned_file(directory, filename, suffix,
312
_current_version=int(re.sub(r"^([0-9]+)[.]([0-9]+).*", r"\1\2", cython_version))):
314
Search a directory for versioned pxd files, e.g. "lib.cython-30.pxd" for a Cython 3.0+ version.
316
@param directory: the directory to search
317
@param filename: the filename without suffix
318
@param suffix: the filename extension including the dot, e.g. ".pxd"
319
@return: the file path if found, or None
321
assert not suffix or suffix[:1] == '.'
322
path_prefix = os.path.join(directory, filename)
324
matching_files = glob.glob(glob.escape(path_prefix) + ".cython-*" + suffix)
325
path = path_prefix + suffix
326
if not os.path.exists(path):
328
best_match = (-1, path)
330
for path in matching_files:
331
versions = _parse_file_version(path)
333
int_version = int(versions[0])
335
if best_match[0] < int_version <= _current_version:
336
best_match = (int_version, path)
342
def decode_filename(filename):
343
if isinstance(filename, bytes):
345
filename_encoding = sys.getfilesystemencoding()
346
if filename_encoding is None:
347
filename_encoding = sys.getdefaultencoding()
348
filename = filename.decode(filename_encoding)
349
except UnicodeDecodeError:
356
_match_file_encoding = re.compile(br"(\w*coding)[:=]\s*([-\w.]+)").search
359
def detect_opened_file_encoding(f, default='UTF-8'):
365
while len(lines) < 3:
368
lines = start.split(b"\n")
372
m = _match_file_encoding(lines[0])
373
if m and m.group(1) != b'c_string_encoding':
374
return m.group(2).decode('iso8859-1')
376
m = _match_file_encoding(lines[1])
378
return m.group(2).decode('iso8859-1')
384
Read past a BOM at the beginning of a source file.
385
This could be added to the scanner, but it's *substantially* easier
386
to keep it at this level.
388
if f.read(1) != '\uFEFF':
392
def open_source_file(source_filename, encoding=None, error_handling=None):
397
f = open(source_filename, 'rb')
398
encoding = detect_opened_file_encoding(f)
400
stream = io.TextIOWrapper(f, encoding=encoding, errors=error_handling)
402
stream = open(source_filename, encoding=encoding, errors=error_handling)
405
if os.path.exists(source_filename):
410
if source_filename.startswith(loader.archive):
411
stream = open_source_from_loader(
412
loader, source_filename,
413
encoding, error_handling)
414
except (NameError, AttributeError):
418
raise FileNotFoundError(source_filename)
423
def open_source_from_loader(loader,
425
encoding=None, error_handling=None):
426
nrmpath = os.path.normpath(source_filename)
427
arcname = nrmpath[len(loader.archive)+1:]
428
data = loader.get_data(arcname)
429
return io.TextIOWrapper(io.BytesIO(data),
431
errors=error_handling)
434
def str_to_number(value):
442
value = int(value, 0)
443
elif value[0] == '0':
444
literal_type = value[1]
445
if literal_type in 'xX':
447
value = strip_py2_long_suffix(value)
448
value = int(value[2:], 16)
449
elif literal_type in 'oO':
451
value = int(value[2:], 8)
452
elif literal_type in 'bB':
454
value = int(value[2:], 2)
457
value = int(value, 8)
459
value = int(value, 0)
460
return -value if is_neg else value
463
def strip_py2_long_suffix(value_str):
465
Python 2 likes to append 'L' to stringified numbers
466
which in then can't process when converting them to numbers.
468
if value_str[-1] in 'lL':
469
return value_str[:-1]
473
def long_literal(value):
474
if isinstance(value, str):
475
value = str_to_number(value)
476
return not -2**31 <= value < 2**31
479
@try_finally_contextmanager
480
def captured_fd(stream=2, encoding=None):
481
orig_stream = os.dup(stream)
483
with tempfile.TemporaryFile(mode="a+b") as temp_file:
484
def read_output(_output=[b'']):
485
if not temp_file.closed:
487
_output[0] = temp_file.read()
490
os.dup2(temp_file.fileno(), stream)
492
result = read_output()
493
return result.decode(encoding) if encoding else result
497
os.dup2(orig_stream, stream)
500
os.close(orig_stream)
503
def get_encoding_candidates():
504
candidates = [sys.getdefaultencoding()]
505
for stream in (sys.stdout, sys.stdin, sys.__stdout__, sys.__stdin__):
506
encoding = getattr(stream, 'encoding', None)
508
if encoding is not None and encoding not in candidates:
509
candidates.append(encoding)
513
def prepare_captured(captured):
514
captured_bytes = captured.strip()
515
if not captured_bytes:
517
for encoding in get_encoding_candidates():
519
return captured_bytes.decode(encoding)
520
except UnicodeDecodeError:
523
return captured_bytes.decode('latin-1')
526
def print_captured(captured, output, header_line=None):
527
captured = prepare_captured(captured)
530
output.write(header_line)
531
output.write(captured)
534
def print_bytes(s, header_text=None, end=b'\n', file=sys.stdout, flush=True):
536
file.write(header_text)
547
def __init__(self, elements=()):
550
self.update(elements)
553
return iter(self._list)
555
def update(self, elements):
560
if e not in self._set:
565
return bool(self._set)
567
__nonzero__ = __bool__
572
def add_metaclass(metaclass):
573
"""Class decorator for creating a class with a metaclass."""
575
orig_vars = cls.__dict__.copy()
576
slots = orig_vars.get('__slots__')
577
if slots is not None:
578
if isinstance(slots, str):
580
for slots_var in slots:
581
orig_vars.pop(slots_var)
582
orig_vars.pop('__dict__', None)
583
orig_vars.pop('__weakref__', None)
584
return metaclass(cls.__name__, cls.__bases__, orig_vars)
588
def raise_error_if_module_name_forbidden(full_module_name):
590
if full_module_name == 'cython' or full_module_name.startswith('cython.'):
591
raise ValueError('cython is a special module, cannot be used as a module name')
594
def build_hex_version(version_string):
596
Parse and translate public version identifier like '4.3a1' into the readable hex representation '0x040300A1' (like PY_VERSION_HEX).
598
SEE: https://peps.python.org/pep-0440/#public-version-identifiers
603
release_status = 0xF0
604
for segment in re.split(r'(\D+)', version_string):
605
if segment in ('a', 'b', 'rc'):
606
release_status = {'a': 0xA0, 'b': 0xB0, 'rc': 0xC0}[segment]
607
digits = (digits + [0, 0])[:3]
608
elif segment in ('.dev', '.pre', '.post'):
611
digits.append(int(segment))
613
digits = (digits + [0] * 3)[:4]
614
digits[3] += release_status
619
hexversion = (hexversion << 8) + digit
621
return '0x%08X' % hexversion
624
def write_depfile(target, source, dependencies):
625
src_base_dir = os.path.dirname(source)
627
if not src_base_dir.endswith(os.sep):
628
src_base_dir += os.sep
631
for fname in dependencies:
632
if fname.startswith(src_base_dir):
634
newpath = os.path.relpath(fname, cwd)
637
newpath = os.path.abspath(fname)
639
newpath = os.path.abspath(fname)
640
paths.append(newpath)
642
depline = os.path.relpath(target, cwd) + ": \\\n "
643
depline += " \\\n ".join(paths) + "\n"
645
with open(target+'.dep', 'w') as outfile:
646
outfile.write(depline)
650
print("Cython version %s" % cython_version)
653
if sys.stderr.isatty() or sys.stdout == sys.stderr:
655
if os.fstat(1) == os.fstat(2):
659
sys.stderr.write("Cython version %s\n" % cython_version)
662
def normalise_float_repr(float_str):
664
Generate a 'normalised', simple digits string representation of a float value
665
to allow string comparisons. Examples: '.123', '123.456', '123.'
667
str_value = float_str.lower().lstrip('0')
670
if 'E' in str_value or 'e' in str_value:
671
str_value, exp = str_value.split('E' if 'E' in str_value else 'e', 1)
675
num_int_digits = str_value.index('.')
676
str_value = str_value[:num_int_digits] + str_value[num_int_digits + 1:]
678
num_int_digits = len(str_value)
679
exp += num_int_digits
683
+ '0' * (exp - len(str_value))
689
return result if result != '.' else '.0'