| 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 |