Python Forum
None collapsing Tuple at 1 value nor need the , at 1 val
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
None collapsing Tuple at 1 value nor need the , at 1 val
#1
name: tup
description: Alternative for tuple that does not collapse nor need a comma when at 1 val and operates within guide lines of tuple. Intentionally left it as
near tuple usage as possible but could add add remove append methods and instance return tuple state.

reason: Python has so rewarding and found this to be very useful for myself so shared here, also if doing code review that means someone unlike me being
a noob could make even more better probally so I am tossing it here.

how to use: from tupled import tup at top of source file. assign name = tup()

edit: I am on Linux so if on windows just thought of may run into write permissions, should be fine on both but if you so can change line to this:

REPLACE: TUP_FOLDER = ".tup"
um WITH THIS: TUP_FOLDER = os.path.join(os.path.expanduser("~"), ".tup")

# tupled.py — smart, auto-promoting persistent container that mimics tuple behavior
import os

TUP_FOLDER = ".tup"

def clear_all_tups_on_launch():
    if os.path.exists(TUP_FOLDER):
        for file in os.listdir(TUP_FOLDER):
            path = os.path.join(TUP_FOLDER, file)
            if os.path.isfile(path):
                os.remove(path)

clear_all_tups_on_launch()

class tup:
    def __init__(self, *initial_values, name=None):
        self.name = name
        self.value = None

        if initial_values:
            self._add(*initial_values)
        elif self.name:
            self._load()

    def __call__(self, *args):
        if not args:
            return self.get()
        return self._add(*args)

    def get(self):
        if isinstance(self.value, tuple):
            return self.value
        elif self.value is not None:
            return (self.value,)
        return None

    def _add(self, *args):
        if not args:
            return self.get()

        if self.value is None:
            self.value = args[0] if len(args) == 1 else args
        elif isinstance(self.value, tuple):
            self.value += args
        else:
            self.value = (self.value,) + args

        self._write()
        return self.get()

    def _remove(self, item):
        if self.value is None:
            return self.get()

        current = self.get()
        if isinstance(current, tuple):
            filtered = tuple(x for x in current if x != item)
            if len(filtered) == 0:
                self.value = None
            else:
                self.value = filtered[0] if len(filtered) == 1 else filtered
        elif current == item:
            self.value = None

        self._write()
        return self.get()

    def _write(self):
        if not self.name:
            return  # Skip writing if no name set
        os.makedirs(TUP_FOLDER, exist_ok=True)
        path = os.path.join(TUP_FOLDER, self.name)

        if self.value is None:
            if os.path.exists(path):
                os.remove(path)
            return

        with open(path, "w") as f:
            for v in self.get():
                f.write(f"{repr(v)}\n")


    def __add__(self, other):
        new = tup(name=None)  # don't persist this by default
        new.value = self.get()
        if new.value is None:
            new.value = (other,)
        else:
            new.value += (other,)
        return new

    def __iadd__(self, other):
        if self.value is None:
            self.value = (other,)
        else:
            self.value += (other,)
        self._write()
        return self



    def _load(self):
        path = os.path.join(TUP_FOLDER, self.name)
        if os.path.exists(path):
            with open(path, "r") as f:
                lines = [eval(line.strip()) for line in f if line.strip()]
                if len(lines) == 1:
                    self.value = lines[0]
                elif lines:
                    self.value = tuple(lines)

    def __iter__(self):
        return iter(self.get())

    def __getitem__(self, index):
        result = self.get()[index]
        if isinstance(index, slice):
            if isinstance(result, tuple) and len(result) == 1:
                return result[0]  # return bare value
            return result
        return result

    def __len__(self):
        val = self.get()
        return len(val) if val is not None else 0

    def __contains__(self, item):
        return item in self.get()

    def __eq__(self, other):
        return self.get() == other

    def __getattr__(self, attr):
        # Avoid intercepting real class methods like 'add', 'remove', etc.
        if attr in self.__class__.__dict__:
            raise AttributeError(f"'tup' object has no attribute '{attr}'")

        val = self.get()
        if val is None:
            raise AttributeError(f"'tup' object has no value yet, so it has no attribute '{attr}'")

        return getattr(val, attr)

    def as_tuple(self):
        return self.get()

    @classmethod
    def from_tuple(cls, name, tup_data):
        t = cls(name)
        t(*tup_data)
        return t

    def __repr__(self):
        val = self.get()
        if val is None:
            return f"tup({self.name!r})"
        elif isinstance(val, tuple):
            if len(val) == 1:
                return f"({repr(val[0])})"  # No comma!
            else:
                return repr(val)

    def as_list(self):
        return list(self.get() or [])

    def as_set(self):
        return set(self.get() or [])

    def as_dict(self):
        return dict(self.get() or [])
[python]
Larz60+ write Jul-20-2025, 03:56 AM:
Please post all code, output and errors (it it's entirety) between their respective tags. Refer to BBCode help topic on how to post. Use the "Preview Post" button to make sure the code is presented as you expect before hitting the "Post Reply/Thread" button.
Tags have been added this time, please use on future posts.

Attached Files

.py   tupled.py (Size: 4.51 KB / Downloads: 0)
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020