Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modify arguments to typing.NamedTuple prior to instance creation

I was looking forward to using the somewhat new typing.NamedTuple class, which allows the creation of named tuple classes using the usual Python class syntax (including the ability to add docstrings and methods, provide default values, type hints, etc etc).

However: the class at the bottom is producing the following error message:

AttributeError: Cannot overwrite NamedTuple attribute __new__

From this I gather just what it says: overriding __new__ is a still no-no. This is very disappointing.

The "old way" of going about this would be to inherit from a named tuple class, but this requires what I consider to be some ugly boilerplate code:

from collections import namedtuple

class FormatSpec(namedtuple('FormatSpecBase', 'fill align sign alt zero '
                                              'width comma decimal precision type')):
    __slots__ = ()
    def __new__(cls, fill, align, sign, alt, zero,
                width, comma, decimal, precision, type):
        to_int=lambda x: int(x) if x is not None else x
        zero=to_int(zero)
        width=to_int(width)
        precision=to_int(precision)
        return super().__new__(cls, fill, align, sign, alt, zero,
                               width, comma, decimal, precision, type)

FormatSpec.__doc__=_FormatSpec.__doc__.replace('FormatSpecBase','FormatSpec')

Is there some other alternate way I can cast the zero, width, and precision arguments below to int prior to the creation of the named tuple, but still using the same class creation syntax? Or am I stuck using the old way?

from typing import NamedTuple, Optional

class FormatSpec(NamedTuple):
    """Represents a string that conforms to the [Format Specification
    Mini-Language][1] in the string module.

    [1]: https://docs.python.org/3/library/string.html#formatspec
    """
    fill: Optional[str]
    align: Optional[str]
    sign: Optional[str]
    alt: Optional[str]
    zero: Optional[int]
    width: Optional[int]
    comma: Optional[str]
    decimal: Optional[str]
    precision: Optional[int]
    type: str
    def __new__(cls, fill, align, sign, alt, zero, width, comma, decimal, precision, type):
        to_int=lambda x: int(x) if x is not None else x
        zero=to_int(zero)
        width=to_int(width)
        precision=to_int(precision)
        return super().__new__(cls, fill, align, sign, alt, zero,
                               width, comma, decimal, precision, type)
    def join(self):
        return ''.join('{!s}'.format(s) for s in self if s is not None)
    def __format__(self, format_spec):
        try:
            return format(self.join(), format_spec)
        except (TypeError, ValueError):
            return super().__format__(format_spec)
like image 584
Rick supports Monica Avatar asked Sep 22 '17 16:09

Rick supports Monica


1 Answers

One way would be to split this up into two classes, and do the arguments modification in the child class:

from typing import NamedTuple, Optional

class FormatSpecBase(NamedTuple):
    """Represents a string that conforms to the [Format Specification
    Mini-Language][1] in the string module.

    [1]: https://docs.python.org/3/library/string.html#formatspec
    """
    fill: Optional[str]
    align: Optional[str]
    sign: Optional[str]
    alt: Optional[str]
    zero: Optional[int]
    width: Optional[int]
    comma: Optional[str]
    decimal: Optional[str]
    precision: Optional[int]
    type: str
    def join(self):
        return ''.join('{!s}'.format(s) for s in self if s is not None)
    def __format__(self, format_spec):
        try:
            return format(self.join(), format_spec)
        except (TypeError, ValueError):
            return super().__format__(format_spec)


class FormatSpec(FormatSpecBase):
    __slots__ = ()
    def __new__(cls, fill, align, sign, alt, zero, width, comma, decimal, precision, type):
        to_int=lambda x: int(x) if x is not None else x
        zero=to_int(zero)
        width=to_int(width)
        precision=to_int(precision)
        return super().__new__(cls, fill, align, sign, alt, zero,
                                    width, comma, decimal, precision, type)

I don't much care for this approach, but at least it is more readable than the "old way" (even though it still needs that hanging __slots__ nonsense).

Another way would be a factory:

def MakeFormatSpec(cls, fill, align, sign, alt, zero,
                   width, comma, decimal, precision, type):
    to_int=lambda x: int(x) if x is not None else x
    zero=to_int(zero)
    width=to_int(width)
    precision=to_int(precision)
    return FormatSpec(fill, align, sign, alt, zero,
                      width, comma, decimal, precision, type)

fspec = MakeFormatSpec(*parse_format_spec(some_format_spec_string))

...or a factory method:

    @classmethod
    def make(cls, fill, align, sign, alt, zero, width, comma, decimal, precision, type):
        to_int=lambda x: int(x) if x is not None else x
        zero=to_int(zero)
        width=to_int(width)
        precision=to_int(precision)
        return cls(fill, align, sign, alt, zero,
                   width, comma, decimal, precision, type)

fspec = FormatSpec.make(*parse_format_spec(some_format_spec_string))

However, these are both pretty clunky compared to simply being able to do:

fspec = FormatSpec(*parse_format_spec(some_format_spec_string))
like image 50
Rick supports Monica Avatar answered Oct 22 '22 06:10

Rick supports Monica