diff options
Diffstat (limited to 'Tools/c-analyzer/c_analyzer_common')
-rw-r--r-- | Tools/c-analyzer/c_analyzer_common/__init__.py | 19 | ||||
-rw-r--r-- | Tools/c-analyzer/c_analyzer_common/_generate.py | 328 | ||||
-rw-r--r-- | Tools/c-analyzer/c_analyzer_common/files.py | 138 | ||||
-rw-r--r-- | Tools/c-analyzer/c_analyzer_common/info.py | 69 | ||||
-rw-r--r-- | Tools/c-analyzer/c_analyzer_common/known.py | 74 | ||||
-rw-r--r-- | Tools/c-analyzer/c_analyzer_common/util.py | 243 |
6 files changed, 0 insertions, 871 deletions
diff --git a/Tools/c-analyzer/c_analyzer_common/__init__.py b/Tools/c-analyzer/c_analyzer_common/__init__.py deleted file mode 100644 index 888b16ff41..0000000000 --- a/Tools/c-analyzer/c_analyzer_common/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -import os.path - - -PKG_ROOT = os.path.dirname(__file__) -DATA_DIR = os.path.dirname(PKG_ROOT) -REPO_ROOT = os.path.dirname( - os.path.dirname(DATA_DIR)) - -SOURCE_DIRS = [os.path.join(REPO_ROOT, name) for name in [ - 'Include', - 'Python', - 'Parser', - 'Objects', - 'Modules', - ]] - - -# Clean up the namespace. -del os diff --git a/Tools/c-analyzer/c_analyzer_common/_generate.py b/Tools/c-analyzer/c_analyzer_common/_generate.py deleted file mode 100644 index 9b2fc9edb5..0000000000 --- a/Tools/c-analyzer/c_analyzer_common/_generate.py +++ /dev/null @@ -1,328 +0,0 @@ -# The code here consists of hacks for pre-populating the known.tsv file. - -from c_parser.preprocessor import _iter_clean_lines -from c_parser.naive import ( - iter_variables, parse_variable_declaration, find_variables, - ) -from c_parser.info import Variable - -from . import SOURCE_DIRS, REPO_ROOT -from .known import DATA_FILE as KNOWN_FILE, HEADER as KNOWN_HEADER -from .info import UNKNOWN, ID -from .util import write_tsv -from .files import iter_cpython_files - - -POTS = ('char ', 'wchar_t ', 'int ', 'Py_ssize_t ') -POTS += tuple('const ' + v for v in POTS) -STRUCTS = ('PyTypeObject', 'PyObject', 'PyMethodDef', 'PyModuleDef', 'grammar') - - -def _parse_global(line, funcname=None): - line = line.strip() - if line.startswith('static '): - if '(' in line and '[' not in line and ' = ' not in line: - return None, None - name, decl = parse_variable_declaration(line) - elif line.startswith(('Py_LOCAL(', 'Py_LOCAL_INLINE(')): - name, decl = parse_variable_declaration(line) - elif line.startswith('_Py_static_string('): - decl = line.strip(';').strip() - name = line.split('(')[1].split(',')[0].strip() - elif line.startswith('_Py_IDENTIFIER('): - decl = line.strip(';').strip() - name = 'PyId_' + line.split('(')[1].split(')')[0].strip() - elif funcname: - return None, None - - # global-only - elif line.startswith('PyAPI_DATA('): # only in .h files - name, decl = parse_variable_declaration(line) - elif line.startswith('extern '): # only in .h files - name, decl = parse_variable_declaration(line) - elif line.startswith('PyDoc_VAR('): - decl = line.strip(';').strip() - name = line.split('(')[1].split(')')[0].strip() - elif line.startswith(POTS): # implied static - if '(' in line and '[' not in line and ' = ' not in line: - return None, None - name, decl = parse_variable_declaration(line) - elif line.startswith(STRUCTS) and line.endswith(' = {'): # implied static - name, decl = parse_variable_declaration(line) - elif line.startswith(STRUCTS) and line.endswith(' = NULL;'): # implied static - name, decl = parse_variable_declaration(line) - elif line.startswith('struct '): - if not line.endswith(' = {'): - return None, None - if not line.partition(' ')[2].startswith(STRUCTS): - return None, None - # implied static - name, decl = parse_variable_declaration(line) - - # file-specific - elif line.startswith(('SLOT1BINFULL(', 'SLOT1BIN(')): - # Objects/typeobject.c - funcname = line.split('(')[1].split(',')[0] - return [ - ('op_id', funcname, '_Py_static_string(op_id, OPSTR)'), - ('rop_id', funcname, '_Py_static_string(op_id, OPSTR)'), - ] - elif line.startswith('WRAP_METHOD('): - # Objects/weakrefobject.c - funcname, name = (v.strip() for v in line.split('(')[1].split(')')[0].split(',')) - return [ - ('PyId_' + name, funcname, f'_Py_IDENTIFIER({name})'), - ] - - else: - return None, None - return name, decl - - -def _pop_cached(varcache, filename, funcname, name, *, - _iter_variables=iter_variables, - ): - # Look for the file. - try: - cached = varcache[filename] - except KeyError: - cached = varcache[filename] = {} - for variable in _iter_variables(filename, - parse_variable=_parse_global, - ): - variable._isglobal = True - cached[variable.id] = variable - for var in cached: - print(' ', var) - - # Look for the variable. - if funcname == UNKNOWN: - for varid in cached: - if varid.name == name: - break - else: - return None - return cached.pop(varid) - else: - return cached.pop((filename, funcname, name), None) - - -def find_matching_variable(varid, varcache, allfilenames, *, - _pop_cached=_pop_cached, - ): - if varid.filename and varid.filename != UNKNOWN: - filenames = [varid.filename] - else: - filenames = allfilenames - for filename in filenames: - variable = _pop_cached(varcache, filename, varid.funcname, varid.name) - if variable is not None: - return variable - else: - if varid.filename and varid.filename != UNKNOWN and varid.funcname is None: - for filename in allfilenames: - if not filename.endswith('.h'): - continue - variable = _pop_cached(varcache, filename, None, varid.name) - if variable is not None: - return variable - return None - - -MULTILINE = { - # Python/Python-ast.c - 'Load_singleton': 'PyObject *', - 'Store_singleton': 'PyObject *', - 'Del_singleton': 'PyObject *', - 'AugLoad_singleton': 'PyObject *', - 'AugStore_singleton': 'PyObject *', - 'Param_singleton': 'PyObject *', - 'And_singleton': 'PyObject *', - 'Or_singleton': 'PyObject *', - 'Add_singleton': 'static PyObject *', - 'Sub_singleton': 'static PyObject *', - 'Mult_singleton': 'static PyObject *', - 'MatMult_singleton': 'static PyObject *', - 'Div_singleton': 'static PyObject *', - 'Mod_singleton': 'static PyObject *', - 'Pow_singleton': 'static PyObject *', - 'LShift_singleton': 'static PyObject *', - 'RShift_singleton': 'static PyObject *', - 'BitOr_singleton': 'static PyObject *', - 'BitXor_singleton': 'static PyObject *', - 'BitAnd_singleton': 'static PyObject *', - 'FloorDiv_singleton': 'static PyObject *', - 'Invert_singleton': 'static PyObject *', - 'Not_singleton': 'static PyObject *', - 'UAdd_singleton': 'static PyObject *', - 'USub_singleton': 'static PyObject *', - 'Eq_singleton': 'static PyObject *', - 'NotEq_singleton': 'static PyObject *', - 'Lt_singleton': 'static PyObject *', - 'LtE_singleton': 'static PyObject *', - 'Gt_singleton': 'static PyObject *', - 'GtE_singleton': 'static PyObject *', - 'Is_singleton': 'static PyObject *', - 'IsNot_singleton': 'static PyObject *', - 'In_singleton': 'static PyObject *', - 'NotIn_singleton': 'static PyObject *', - # Python/symtable.c - 'top': 'static identifier ', - 'lambda': 'static identifier ', - 'genexpr': 'static identifier ', - 'listcomp': 'static identifier ', - 'setcomp': 'static identifier ', - 'dictcomp': 'static identifier ', - '__class__': 'static identifier ', - # Python/compile.c - '__doc__': 'static PyObject *', - '__annotations__': 'static PyObject *', - # Objects/floatobject.c - 'double_format': 'static float_format_type ', - 'float_format': 'static float_format_type ', - 'detected_double_format': 'static float_format_type ', - 'detected_float_format': 'static float_format_type ', - # Parser/listnode.c - 'level': 'static int ', - 'atbol': 'static int ', - # Python/dtoa.c - 'private_mem': 'static double private_mem[PRIVATE_mem]', - 'pmem_next': 'static double *', - # Modules/_weakref.c - 'weakref_functions': 'static PyMethodDef ', -} -INLINE = { - # Modules/_tracemalloc.c - 'allocators': 'static struct { PyMemAllocatorEx mem; PyMemAllocatorEx raw; PyMemAllocatorEx obj; } ', - # Modules/faulthandler.c - 'fatal_error': 'static struct { int enabled; PyObject *file; int fd; int all_threads; PyInterpreterState *interp; void *exc_handler; } ', - 'thread': 'static struct { PyObject *file; int fd; PY_TIMEOUT_T timeout_us; int repeat; PyInterpreterState *interp; int exit; char *header; size_t header_len; PyThread_type_lock cancel_event; PyThread_type_lock running; } ', - # Modules/signalmodule.c - 'Handlers': 'static volatile struct { _Py_atomic_int tripped; PyObject *func; } Handlers[NSIG]', - 'wakeup': 'static volatile struct { SOCKET_T fd; int warn_on_full_buffer; int use_send; } ', - # Python/dynload_shlib.c - 'handles': 'static struct { dev_t dev; ino_t ino; void *handle; } handles[128]', - # Objects/obmalloc.c - '_PyMem_Debug': 'static struct { debug_alloc_api_t raw; debug_alloc_api_t mem; debug_alloc_api_t obj; } ', - # Python/bootstrap_hash.c - 'urandom_cache': 'static struct { int fd; dev_t st_dev; ino_t st_ino; } ', - } -FUNC = { - # Objects/object.c - '_Py_abstract_hack': 'Py_ssize_t (*_Py_abstract_hack)(PyObject *)', - # Parser/myreadline.c - 'PyOS_InputHook': 'int (*PyOS_InputHook)(void)', - # Python/pylifecycle.c - '_PyOS_mystrnicmp_hack': 'int (*_PyOS_mystrnicmp_hack)(const char *, const char *, Py_ssize_t)', - # Parser/myreadline.c - 'PyOS_ReadlineFunctionPointer': 'char *(*PyOS_ReadlineFunctionPointer)(FILE *, FILE *, const char *)', - } -IMPLIED = { - # Objects/boolobject.c - '_Py_FalseStruct': 'static struct _longobject ', - '_Py_TrueStruct': 'static struct _longobject ', - # Modules/config.c - '_PyImport_Inittab': 'struct _inittab _PyImport_Inittab[]', - } -GLOBALS = {} -GLOBALS.update(MULTILINE) -GLOBALS.update(INLINE) -GLOBALS.update(FUNC) -GLOBALS.update(IMPLIED) - -LOCALS = { - 'buildinfo': ('Modules/getbuildinfo.c', - 'Py_GetBuildInfo', - 'static char buildinfo[50 + sizeof(GITVERSION) + ((sizeof(GITTAG) > sizeof(GITBRANCH)) ? sizeof(GITTAG) : sizeof(GITBRANCH))]'), - 'methods': ('Python/codecs.c', - '_PyCodecRegistry_Init', - 'static struct { char *name; PyMethodDef def; } methods[]'), - } - - -def _known(symbol): - if symbol.funcname: - if symbol.funcname != UNKNOWN or symbol.filename != UNKNOWN: - raise KeyError(symbol.name) - filename, funcname, decl = LOCALS[symbol.name] - varid = ID(filename, funcname, symbol.name) - elif not symbol.filename or symbol.filename == UNKNOWN: - raise KeyError(symbol.name) - else: - varid = symbol.id - try: - decl = GLOBALS[symbol.name] - except KeyError: - - if symbol.name.endswith('_methods'): - decl = 'static PyMethodDef ' - elif symbol.filename == 'Objects/exceptions.c' and symbol.name.startswith(('PyExc_', '_PyExc_')): - decl = 'static PyTypeObject ' - else: - raise - if symbol.name not in decl: - decl = decl + symbol.name - return Variable(varid, 'static', decl) - - -def known_row(varid, decl): - return ( - varid.filename, - varid.funcname or '-', - varid.name, - 'variable', - decl, - ) - - -def known_rows(symbols, *, - cached=True, - _get_filenames=iter_cpython_files, - _find_match=find_matching_variable, - _find_symbols=find_variables, - _as_known=known_row, - ): - filenames = list(_get_filenames()) - cache = {} - if cached: - for symbol in symbols: - try: - found = _known(symbol) - except KeyError: - found = _find_match(symbol, cache, filenames) - if found is None: - found = Variable(symbol.id, UNKNOWN, UNKNOWN) - yield _as_known(found.id, found.vartype) - else: - raise NotImplementedError # XXX incorporate KNOWN - for variable in _find_symbols(symbols, filenames, - srccache=cache, - parse_variable=_parse_global, - ): - #variable = variable._replace( - # filename=os.path.relpath(variable.filename, REPO_ROOT)) - if variable.funcname == UNKNOWN: - print(variable) - if variable.vartype== UNKNOWN: - print(variable) - yield _as_known(variable.id, variable.vartype) - - -def generate(symbols, filename=None, *, - _generate_rows=known_rows, - _write_tsv=write_tsv, - ): - if not filename: - filename = KNOWN_FILE + '.new' - - rows = _generate_rows(symbols) - _write_tsv(filename, KNOWN_HEADER, rows) - - -if __name__ == '__main__': - from c_symbols import binary - symbols = binary.iter_symbols( - binary.PYTHON, - find_local_symbol=None, - ) - generate(symbols) diff --git a/Tools/c-analyzer/c_analyzer_common/files.py b/Tools/c-analyzer/c_analyzer_common/files.py deleted file mode 100644 index b3cd16c8dc..0000000000 --- a/Tools/c-analyzer/c_analyzer_common/files.py +++ /dev/null @@ -1,138 +0,0 @@ -import glob -import os -import os.path - -from . import SOURCE_DIRS, REPO_ROOT - - -C_SOURCE_SUFFIXES = ('.c', '.h') - - -def _walk_tree(root, *, - _walk=os.walk, - ): - # A wrapper around os.walk that resolves the filenames. - for parent, _, names in _walk(root): - for name in names: - yield os.path.join(parent, name) - - -def walk_tree(root, *, - suffix=None, - walk=_walk_tree, - ): - """Yield each file in the tree under the given directory name. - - If "suffix" is provided then only files with that suffix will - be included. - """ - if suffix and not isinstance(suffix, str): - raise ValueError('suffix must be a string') - - for filename in walk(root): - if suffix and not filename.endswith(suffix): - continue - yield filename - - -def glob_tree(root, *, - suffix=None, - _glob=glob.iglob, - ): - """Yield each file in the tree under the given directory name. - - If "suffix" is provided then only files with that suffix will - be included. - """ - suffix = suffix or '' - if not isinstance(suffix, str): - raise ValueError('suffix must be a string') - - for filename in _glob(f'{root}/*{suffix}'): - yield filename - for filename in _glob(f'{root}/**/*{suffix}'): - yield filename - - -def iter_files(root, suffix=None, relparent=None, *, - get_files=os.walk, - _glob=glob_tree, - _walk=walk_tree, - ): - """Yield each file in the tree under the given directory name. - - If "root" is a non-string iterable then do the same for each of - those trees. - - If "suffix" is provided then only files with that suffix will - be included. - - if "relparent" is provided then it is used to resolve each - filename as a relative path. - """ - if not isinstance(root, str): - roots = root - for root in roots: - yield from iter_files(root, suffix, relparent, - get_files=get_files, - _glob=_glob, _walk=_walk) - return - - # Use the right "walk" function. - if get_files in (glob.glob, glob.iglob, glob_tree): - get_files = _glob - else: - _files = _walk_tree if get_files in (os.walk, walk_tree) else get_files - get_files = (lambda *a, **k: _walk(*a, walk=_files, **k)) - - # Handle a single suffix. - if suffix and not isinstance(suffix, str): - filenames = get_files(root) - suffix = tuple(suffix) - else: - filenames = get_files(root, suffix=suffix) - suffix = None - - for filename in filenames: - if suffix and not isinstance(suffix, str): # multiple suffixes - if not filename.endswith(suffix): - continue - if relparent: - filename = os.path.relpath(filename, relparent) - yield filename - - -def iter_files_by_suffix(root, suffixes, relparent=None, *, - walk=walk_tree, - _iter_files=iter_files, - ): - """Yield each file in the tree that has the given suffixes. - - Unlike iter_files(), the results are in the original suffix order. - """ - if isinstance(suffixes, str): - suffixes = [suffixes] - # XXX Ignore repeated suffixes? - for suffix in suffixes: - yield from _iter_files(root, suffix, relparent) - - -def iter_cpython_files(*, - walk=walk_tree, - _files=iter_files_by_suffix, - ): - """Yield each file in the tree for each of the given directory names.""" - excludedtrees = [ - os.path.join('Include', 'cpython', ''), - ] - def is_excluded(filename): - for root in excludedtrees: - if filename.startswith(root): - return True - return False - for filename in _files(SOURCE_DIRS, C_SOURCE_SUFFIXES, REPO_ROOT, - walk=walk, - ): - if is_excluded(filename): - continue - yield filename diff --git a/Tools/c-analyzer/c_analyzer_common/info.py b/Tools/c-analyzer/c_analyzer_common/info.py deleted file mode 100644 index e217380406..0000000000 --- a/Tools/c-analyzer/c_analyzer_common/info.py +++ /dev/null @@ -1,69 +0,0 @@ -from collections import namedtuple -import re - -from .util import classonly, _NTBase - - -UNKNOWN = '???' - -NAME_RE = re.compile(r'^([a-zA-Z]|_\w*[a-zA-Z]\w*|[a-zA-Z]\w*)$') - - -class ID(_NTBase, namedtuple('ID', 'filename funcname name')): - """A unique ID for a single symbol or declaration.""" - - __slots__ = () - # XXX Add optional conditions (tuple of strings) field. - #conditions = Slot() - - @classonly - def from_raw(cls, raw): - if not raw: - return None - if isinstance(raw, str): - return cls(None, None, raw) - try: - name, = raw - filename = None - except ValueError: - try: - filename, name = raw - except ValueError: - return super().from_raw(raw) - return cls(filename, None, name) - - def __new__(cls, filename, funcname, name): - self = super().__new__( - cls, - filename=str(filename) if filename else None, - funcname=str(funcname) if funcname else None, - name=str(name) if name else None, - ) - #cls.conditions.set(self, tuple(str(s) if s else None - # for s in conditions or ())) - return self - - def validate(self): - """Fail if the object is invalid (i.e. init with bad data).""" - if not self.name: - raise TypeError('missing name') - else: - if not NAME_RE.match(self.name): - raise ValueError( - f'name must be an identifier, got {self.name!r}') - - # Symbols from a binary might not have filename/funcname info. - - if self.funcname: - if not self.filename: - raise TypeError('missing filename') - if not NAME_RE.match(self.funcname) and self.funcname != UNKNOWN: - raise ValueError( - f'name must be an identifier, got {self.funcname!r}') - - # XXX Require the filename (at least UNKONWN)? - # XXX Check the filename? - - @property - def islocal(self): - return self.funcname is not None diff --git a/Tools/c-analyzer/c_analyzer_common/known.py b/Tools/c-analyzer/c_analyzer_common/known.py deleted file mode 100644 index dec1e1d2e0..0000000000 --- a/Tools/c-analyzer/c_analyzer_common/known.py +++ /dev/null @@ -1,74 +0,0 @@ -import csv -import os.path - -from c_parser.info import Variable - -from . import DATA_DIR -from .info import ID, UNKNOWN -from .util import read_tsv - - -DATA_FILE = os.path.join(DATA_DIR, 'known.tsv') - -COLUMNS = ('filename', 'funcname', 'name', 'kind', 'declaration') -HEADER = '\t'.join(COLUMNS) - - -# XXX need tests: -# * from_file() - -def from_file(infile, *, - _read_tsv=read_tsv, - ): - """Return the info for known declarations in the given file.""" - known = { - 'variables': {}, - #'types': {}, - #'constants': {}, - #'macros': {}, - } - for row in _read_tsv(infile, HEADER): - filename, funcname, name, kind, declaration = row - if not funcname or funcname == '-': - funcname = None - id = ID(filename, funcname, name) - if kind == 'variable': - values = known['variables'] - if funcname: - storage = _get_storage(declaration) or 'local' - else: - storage = _get_storage(declaration) or 'implicit' - value = Variable(id, storage, declaration) - else: - raise ValueError(f'unsupported kind in row {row}') - value.validate() -# if value.name == 'id' and declaration == UNKNOWN: -# # None of these are variables. -# declaration = 'int id'; -# else: -# value.validate() - values[id] = value - return known - - -def _get_storage(decl): - # statics - if decl.startswith('static '): - return 'static' - if decl.startswith(('Py_LOCAL(', 'Py_LOCAL_INLINE(')): - return 'static' - if decl.startswith(('_Py_IDENTIFIER(', '_Py_static_string(')): - return 'static' - if decl.startswith('PyDoc_VAR('): - return 'static' - if decl.startswith(('SLOT1BINFULL(', 'SLOT1BIN(')): - return 'static' - if decl.startswith('WRAP_METHOD('): - return 'static' - # public extern - if decl.startswith('extern '): - return 'extern' - if decl.startswith('PyAPI_DATA('): - return 'extern' - # implicit or local - return None diff --git a/Tools/c-analyzer/c_analyzer_common/util.py b/Tools/c-analyzer/c_analyzer_common/util.py deleted file mode 100644 index 43d0bb6e66..0000000000 --- a/Tools/c-analyzer/c_analyzer_common/util.py +++ /dev/null @@ -1,243 +0,0 @@ -import csv -import subprocess - - -_NOT_SET = object() - - -def run_cmd(argv, **kwargs): - proc = subprocess.run( - argv, - #capture_output=True, - #stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, - text=True, - check=True, - **kwargs - ) - return proc.stdout - - -def read_tsv(infile, header, *, - _open=open, - _get_reader=csv.reader, - ): - """Yield each row of the given TSV (tab-separated) file.""" - if isinstance(infile, str): - with _open(infile, newline='') as infile: - yield from read_tsv(infile, header, - _open=_open, - _get_reader=_get_reader, - ) - return - lines = iter(infile) - - # Validate the header. - try: - actualheader = next(lines).strip() - except StopIteration: - actualheader = '' - if actualheader != header: - raise ValueError(f'bad header {actualheader!r}') - - for row in _get_reader(lines, delimiter='\t'): - yield tuple(v.strip() for v in row) - - -def write_tsv(outfile, header, rows, *, - _open=open, - _get_writer=csv.writer, - ): - """Write each of the rows to the given TSV (tab-separated) file.""" - if isinstance(outfile, str): - with _open(outfile, 'w', newline='') as outfile: - return write_tsv(outfile, header, rows, - _open=_open, - _get_writer=_get_writer, - ) - - if isinstance(header, str): - header = header.split('\t') - writer = _get_writer(outfile, delimiter='\t') - writer.writerow(header) - for row in rows: - writer.writerow('' if v is None else str(v) - for v in row) - - -class Slot: - """A descriptor that provides a slot. - - This is useful for types that can't have slots via __slots__, - e.g. tuple subclasses. - """ - - __slots__ = ('initial', 'default', 'readonly', 'instances', 'name') - - def __init__(self, initial=_NOT_SET, *, - default=_NOT_SET, - readonly=False, - ): - self.initial = initial - self.default = default - self.readonly = readonly - - # The instance cache is not inherently tied to the normal - # lifetime of the instances. So must do something in order to - # avoid keeping the instances alive by holding a reference here. - # Ideally we would use weakref.WeakValueDictionary to do this. - # However, most builtin types do not support weakrefs. So - # instead we monkey-patch __del__ on the attached class to clear - # the instance. - self.instances = {} - self.name = None - - def __set_name__(self, cls, name): - if self.name is not None: - raise TypeError('already used') - self.name = name - try: - slotnames = cls.__slot_names__ - except AttributeError: - slotnames = cls.__slot_names__ = [] - slotnames.append(name) - self._ensure___del__(cls, slotnames) - - def __get__(self, obj, cls): - if obj is None: # called on the class - return self - try: - value = self.instances[id(obj)] - except KeyError: - if self.initial is _NOT_SET: - value = self.default - else: - value = self.initial - self.instances[id(obj)] = value - if value is _NOT_SET: - raise AttributeError(self.name) - # XXX Optionally make a copy? - return value - - def __set__(self, obj, value): - if self.readonly: - raise AttributeError(f'{self.name} is readonly') - # XXX Optionally coerce? - self.instances[id(obj)] = value - - def __delete__(self, obj): - if self.readonly: - raise AttributeError(f'{self.name} is readonly') - self.instances[id(obj)] = self.default # XXX refleak? - - def _ensure___del__(self, cls, slotnames): # See the comment in __init__(). - try: - old___del__ = cls.__del__ - except AttributeError: - old___del__ = (lambda s: None) - else: - if getattr(old___del__, '_slotted', False): - return - - def __del__(_self): - for name in slotnames: - delattr(_self, name) - old___del__(_self) - __del__._slotted = True - cls.__del__ = __del__ - - def set(self, obj, value): - """Update the cached value for an object. - - This works even if the descriptor is read-only. This is - particularly useful when initializing the object (e.g. in - its __new__ or __init__). - """ - self.instances[id(obj)] = value - - -class classonly: - """A non-data descriptor that makes a value only visible on the class. - - This is like the "classmethod" builtin, but does not show up on - instances of the class. It may be used as a decorator. - """ - - def __init__(self, value): - self.value = value - self.getter = classmethod(value).__get__ - self.name = None - - def __set_name__(self, cls, name): - if self.name is not None: - raise TypeError('already used') - self.name = name - - def __get__(self, obj, cls): - if obj is not None: - raise AttributeError(self.name) - # called on the class - return self.getter(None, cls) - - -class _NTBase: - - __slots__ = () - - @classonly - def from_raw(cls, raw): - if not raw: - return None - elif isinstance(raw, cls): - return raw - elif isinstance(raw, str): - return cls.from_string(raw) - else: - if hasattr(raw, 'items'): - return cls(**raw) - try: - args = tuple(raw) - except TypeError: - pass - else: - return cls(*args) - raise NotImplementedError - - @classonly - def from_string(cls, value): - """Return a new instance based on the given string.""" - raise NotImplementedError - - @classmethod - def _make(cls, iterable): # The default _make() is not subclass-friendly. - return cls.__new__(cls, *iterable) - - # XXX Always validate? - #def __init__(self, *args, **kwargs): - # self.validate() - - # XXX The default __repr__() is not subclass-friendly (where the name changes). - #def __repr__(self): - # _, _, sig = super().__repr__().partition('(') - # return f'{self.__class__.__name__}({sig}' - - # To make sorting work with None: - def __lt__(self, other): - try: - return super().__lt__(other) - except TypeError: - if None in self: - return True - elif None in other: - return False - else: - raise - - def validate(self): - return - - # XXX Always validate? - #def _replace(self, **kwargs): - # obj = super()._replace(**kwargs) - # obj.validate() - # return obj |