"""
The miff-muffet-moof module.
"""
import io as _io
import sys as _sys
import re as _re
from pathlib import Path as _Path
import contextlib as _contextlib
import functools as _funtiontools
import ctypes as _ctypes
[docs]
def as_path_or_buffer(file):
"""Normalise filenames to `pathlib.Path`, leaving streams untouched."""
return file if isinstance(file, _io.IOBase) else _Path(file)
[docs]
def as_path_or_readable_buffer(file):
"""Normalise filenames to `pathlib.Path`, and streams to `io.StringIO`.
Streams need to be re-readable in |cslug|. An `io.StringIO` is via
``file.getvalue()`` - everything else generally isn't. The goal of using
`io` streams is only to prevent strings of source code from being confused
for string filenames - not, as is the more normal usage, to avoid holding
large files in memory.
"""
if isinstance(file, _io.TextIOBase):
if hasattr(file, "getvalue"):
return file
return _io.StringIO(file.read())
return _Path(file)
def _read(path, mode="r"):
if isinstance(path, _io.IOBase):
if hasattr(path, "getvalue"):
return path.getvalue(), None
return path.read(), None
with open(path, mode, encoding="utf-8") as f:
return f.read(), path
[docs]
def read(path, mode="r"):
r"""Read a path or a stream.
Line endings are normalised to Unix ``'\n'`` if :py:`mode == 'r'` so as to
be consistent with `open`.
"""
text, path = _read(path, mode)
if mode == "r":
text = _re.sub("\r\n?", "\n", text)
return text, path
[docs]
def write(path, *data, mode="w"):
"""Write to a path or a stream.
Args:
path (str or os.PathLike or io.IOBase):
File to write to.
*data (str or bytes):
Data to write.
mode (str):
A mode to be passed to `open`, ignored if **path** is a stream.
Returns:
int: The number of characters written.
If **path** is a stream it is simply written to without closing it
afterwards. If **path** is a filename then it is opened, written to, then
closed.
"""
if isinstance(path, _io.IOBase):
return path.writelines(data)
with open(path, mode, encoding="utf-8") as f:
return f.writelines(data)
[docs]
def anchor(*paths):
"""Replace relative paths with frozen paths relative to ``__file__``\\ 's
parent.
:param paths: Path(s) to freeze or pseudo files.
:type paths: str or os.PathLike or io.IOBase
:return: List of modified paths.
:rtype: list
Pseudo files (`io.IOBase`) and absolute paths are left unchanged. Use this
function to make your code working-dir independent.
"""
paths = map(as_path_or_buffer, paths)
file = _sys._getframe().f_back.f_globals.get("__file__")
if file is None or file[0] == "<":
return [_Path.cwd() / i for i in paths]
parent = _Path(file).parent
return [
parent / i if isinstance(i, _Path) and not i.is_absolute() else i
for i in paths
]
[docs]
def hide_from_PATH(name):
"""Modify ``PATH`` from `os.environ` so that **name** can't be found.
Args:
name (str): The executable name to hide.
Returns:
str: The original value of :py:`os.environ['PATH']`.
"""
import os
import shutil
old = os.environ["PATH"]
paths = old.split(os.pathsep)
paths = [i for i in paths if shutil.which(name, path=i) is None]
os.environ["PATH"] = os.pathsep.join(paths)
return old
[docs]
def flatten(iterable, types=(tuple, list), initial=None):
"""Collapse nested iterables into one flat `list`.
Args:
iterable:
Nested container.
types:
Type(s) to be collapsed. This argument is passed directly to
`isinstance`.
initial:
A pre-existing `list` to append to. An empty list is created if one
is not supplied.
Returns:
list: The flattened output.
::
>>> flatten([[1, 2], 3, [4, [5]]])
[1, 2, 3, 4, 5]
>>> flatten(1.0)
[1.0]
>>> flatten([(1, 2), 3, (4, 5)])
[1, 2, 3, 4, 5]
>>> flatten([(1, 2), 3, (4, 5)], types=list)
[(1, 2), 3, (4, 5)]
>>> flatten([(1, 2), 3, (4, 5)], initial=[6, 7])
[6, 7, 1, 2, 3, 4, 5]
"""
if initial is None:
initial = []
if isinstance(iterable, types):
for i in iterable:
flatten(i, types, initial)
else:
initial.append(iterable)
return initial
[docs]
@_contextlib.contextmanager
def block_compile():
"""Temporarily block |cslug| compilation.
A context manager to temporarily set the ``CC`` environment variable to
``!block`` which is a signal to |cslug| that it is not allowed to use any
C compiler.
::
>>> from cslug import cc, misc
>>> cc()
'c:\\MinGW\\bin\\gcc.EXE'
>>> with misc.block_compile():
... cc()
cslug.exceptions.BuildBlockedError: The build was blocked by the
environment variable `CC=block`.
This is only meant for testing.
"""
import os
old = os.environ.get("CC")
os.environ["CC"] = "!block"
try:
yield
finally:
if old is None:
del os.environ["CC"]
else:
os.environ["CC"] = old
[docs]
def array_typecode(c_name):
"""Choose a type code for `array.array`.
Args:
c_name (str): The name you would use in C to define the type.
Returns:
str: Any of `array.typecodes`.
Use this function to normalise aliases and platform specific exact types.
Examples:
>>> array_typecode("long")
'l'
>>> array_typecode("double")
'd'
>>> array_typecode("uint64_t")
'Q'
>>> array_typecode("size_t")
'Q'
"""
out = _array_typecode(c_name)
if out is None:
raise ValueError(f"Unrecognised or unsupported type '{c_name}'.")
return out
@_funtiontools.lru_cache()
def _array_typecode(c_name: str):
"""The workhorse behind `array_typecode`.
The only difference is that this function returns None rather than raising
an error if it can't find a typecode. Splitting into two allows this
function to call itself with normalised inputs without having to remember
the original value of **c_name** to include in any error messages.
"""
import ctypes
if c_name.startswith("unsigned "):
# Unsigned typecodes are the same as their signed types but upper-cased.
return _array_typecode(c_name[9:]).upper()
# XXX: Can be replaced with str.removeprefix() in Python 3.9
# Remove `signed ` prefix.
c_name = _re.fullmatch(r"(?:signed )?(.*)", c_name).group(1)
# Remove `c_` prefix and `_t` suffix.
c_name = _re.fullmatch(r"(?:c_)?(.+?)(?:_t)?", c_name).group(1)
# Ignore the word ` int` if there are other words in front of it.
c_name = _re.fullmatch(r"(.+?)(?: int)?", c_name).group(1)
# Collapse any spaces.
c_name = c_name.replace(" ", "")
if c_name in _ARRAY_TYPECODES:
return _ARRAY_TYPECODES[c_name]
# ctypes has already normalised exact types to the more standard types.
# e.g. `ctypes.c_int16` is just an alias for `ctypes.c_short` so
# `ctypes.c_int16.__name__` will give `c_short`.
ctypes_name = "c_" + c_name
if hasattr(ctypes, ctypes_name):
c_name = getattr(ctypes, ctypes_name).__name__
c_name = c_name[2:]
if c_name in _ARRAY_TYPECODES:
return _ARRAY_TYPECODES[c_name]
if c_name.startswith("u") and c_name[1:] in _ARRAY_TYPECODES:
return _ARRAY_TYPECODES[c_name[1:]].upper()
_ARRAY_TYPECODES = {
"char": "b",
"short": "h",
"int": "i",
"long": "l",
"longlong": "q",
"float": "f",
"double": "d",
"byte": "b",
}
_ARRAY_TYPECODES["ssize"] = array_typecode(_ctypes.c_ssize_t.__name__)
_ARRAY_TYPECODES["size"] = _ARRAY_TYPECODES["ssize"].upper()