0

I wanted to make a script in order to manage my friends and I, walkie talkies. so I created 4 classes: a Range is a modification that can upgrade the walkie talkie a Walkie Talkie describe the radio and its possible mods(Range) a Person describe me or my friends and their radios a TalkieRangeData is a class to handle all of this

Here is my code:

import json
import base64
import os


def is_base64(s):
    """Check if a parameter is encoded in base64."""
    try:
        base64.b64decode(s)
        return True and s[-1] == "="
    except:
        return False


def to_base64(s):
    """Encode a parameter in base64 if it is not already encoded."""
    if not is_base64(s):
        s = base64.b64encode(s.encode()).decode()
    return s


def from_base64(s):
    """Decode a parameter from base64 if it is encoded."""
    if is_base64(s):
        s = base64.b64decode(s).decode()
    return s


class Range:
    def __init__(self, mod, minrange, maxrange, commentary):
        self.mod = mod
        self.minrange = minrange
        self.maxrange = maxrange
        self.commentary = to_base64(commentary)

    def update_range(
        self, new_mod=None, new_min=None, new_max=None, new_commentary=None
    ):
        if new_mod:
            self.mod = new_mod
        if new_min:
            self.minrange = new_min
        if new_max:
            self.maxrange = new_max
        if new_commentary:
            self.commentary = to_base64(new_commentary)

    def __repr__(self):
        return f"Mod: {self.mod}\n\tMin: {self.minrange}\n\tMax: {self.maxrange}\n\tCommentary: {from_base64(self.commentary)} "

    def toJSON(self):
        return {
            "mod": self.mod,
            "minrange": self.minrange,
            "maxrange": self.maxrange,
            "commentary": self.commentary,
        }


class WalkieTalkie:
    def __init__(self, model, general_commentary, ranges=list()):
        self.model = model
        self.generalCommentary = to_base64(general_commentary)
        self.ranges = list()
        for r in ranges:
            if isinstance(r, Range):
                self.ranges.append(r)
            else:
                self.ranges.append(Range(*r))

    def add_range(self, mod, minrange, maxrange, commentary):
        self.ranges.append(Range(mod, minrange, maxrange, to_base64(commentary)))

    def update_range(
        self, mod, new_mod=None, new_min=None, new_max=None, new_commentary=None
    ):
        range = next((r for r in self.ranges if r.mod == mod), None)
        if range:
            range.update_range(new_mod, new_min, new_max, new_commentary)
        else:
            raise ValueError("No range with that mod name was found.")

    def delete_range(self, mod):
        range = next((r for r in self.ranges if r.mod == mod), None)
        if range:
            self.ranges.remove(range)
        else:
            raise ValueError("No range with that mod name was found.")

    def __repr__(self):
        ranges_str = "\n".join([f"{range}" for range in self.ranges])
        return f"Model: {self.model}\nGeneral Commentary: {from_base64(self.generalCommentary)}\nRanges:\n{ranges_str}"

    def toJSON(self):
        return {
            "model": self.model,
            "generalCommentary": self.generalCommentary,
            "ranges": [r.toJSON() for r in self.ranges],
        }


class Person:
    def __init__(self, name, color, latitude, longitude, walkie_talkies=list()):
        self.name = name
        self.color = color
        self.location = {"latitude": latitude, "longitude": longitude}
        self.walkie_talkies = walkie_talkies

    def add_walkie_talkie(self, model, general_commentary, ranges=list()):
        wt = WalkieTalkie(model, general_commentary, ranges)
        self.walkie_talkies.append(wt)

    def add_walkie_talkie2(self, walkie_talkie: WalkieTalkie):
        self.walkie_talkies.append(walkie_talkie)

    def update_walkie_talkie(
        self, model, new_model=None, new_general_commentary=None, new_ranges=None
    ):
        wt_to_update = next(wt for wt in self.walkie_talkies if wt.model == model)
        if new_model:
            wt_to_update.model = new_model
        if new_general_commentary:
            wt_to_update.generalCommentary = to_base64(new_general_commentary)
        if new_ranges:
            wt_to_update.ranges = new_ranges

    def delete_walkie_talkie(self, model):
        self.walkie_talkies = [wt for wt in self.walkie_talkies if wt.model != model]

    def __repr__(self):
        walkie_talkies_str = "\n".join(
            [f"{walkie_talkie}" for walkie_talkie in self.walkie_talkies]
        )
        return (
            f"Name: {self.name}\nLocation: ({self.location['latitude']}, {self.location['longitude']})\nWalkie "
            f"Talkies:\n{walkie_talkies_str} "
        )


class TalkieRangeData:
    def __init__(self, filepath):
        self.filepath = filepath
        self.people = self.read_data()

    def read_data(self):
        # if file path does not exist, create it
        if not os.path.exists(self.filepath):
            with open(self.filepath, "w") as f:
                f.write('\{"people": []\}')

        with open(self.filepath, "r") as f:
            try:
                data = json.load(f)
                return [
                    Person(
                        person["name"],
                        person["color"],
                        person["location"]["latitude"],
                        person["location"]["longitude"],
                        [
                            WalkieTalkie(
                                wt["model"],
                                from_base64(wt["generalCommentary"]),
                                [
                                    Range(
                                        r["mod"],
                                        r["minrange"],
                                        r["maxrange"],
                                        from_base64(r["commentary"]),
                                    )
                                    for r in wt["ranges"]
                                ],
                            )
                            for wt in person["walkieTalkies"]
                        ],
                    )
                    for person in data["people"]
                ]
            except json.decoder.JSONDecodeError:
                return list()

    def add_person(self, name, color, latitude, longitude, overwrite=False):
        if not overwrite:
            existing_person = next(
                (person for person in self.people if person.name == name), None
            )
            if existing_person:
                raise ValueError(
                    "A person with that name already exists. Set `overwrite` to True if you want to update the existing person."
                )
        person = Person(name, color, latitude, longitude)
        self.people.append(person)
        self.update_json()

    def add_person2(self, pperson: Person, overwrite=False):
        if not overwrite:
            existing_person = next(
                (person for person in self.people if person.name == pperson.name), None
            )
            if existing_person:
                raise ValueError(
                    "A person with that name already exists. Set `overwrite` to True if you want to update the existing person."
                )
        self.people.append(pperson)
        self.update_json()

    def update_json(self):
        data = {
            "people": [
                {
                    "name": person.name,
                    "color": person.color,
                    "location": person.location,
                    "walkieTalkies": [wt.toJSON() for wt in person.walkie_talkies],
                }
                for person in self.people
            ]
        }
        with open(self.filepath, "w") as f:
            json.dump(data, f, indent=2)

    def update_person(
        self,
        name,
        new_name=None,
        new_latitude=None,
        new_longitude=None,
        new_walkie_talkies=None,
    ):
        person_to_update = next(person for person in self.people if person.name == name)
        if new_name:
            person_to_update.name = new_name
        if new_latitude:
            person_to_update.location["latitude"] = new_latitude
        if new_longitude:
            person_to_update.location["longitude"] = new_longitude
        if new_walkie_talkies:
            person_to_update.walkie_talkies = new_walkie_talkies
        self.update_json()

    def delete_person(self, name):
        self.people = [person for person in self.people if person.name != name]
        self.update_json()

    def __repr__(self):
        return f"TalkieRangeData"

and here is my main.py file that I use to generate a kml file from input data

import json
import os
import simplekml

from TalkieRangeData import (
    TalkieRangeData,
    to_base64,
    from_base64,
    Person,
    WalkieTalkie,
    Range,
)
from polycircles import polycircles


def load_data():
    if os.path.exists("talkies.json"):
        trd = TalkieRangeData("talkies.json")
    else:
        print(
            "talkies.json does not exist. Let's create it.First we need to create at least one user."
        )
        name = input("Enter the name of the person: ")
        color = input("Enter the color of the person: ")
        latitude = input("Enter the latitude of the person: ")
        longitude = input("Enter the longitude of the person: ")
        model = input("Enter the model of the walkie talkie: ")
        general_commentary = input(
            "Enter the general commentary of the walkie talkie: "
        )
        mod = input("Enter the mod of the range: ")
        rangemin = input("Enter the min of the range: ")
        rangemax = input("Enter the max of the range: ")
        commentary = input("Enter the commentary of the range: ")
        json_data = {
            "people": [
                {
                    "name": name,
                    "location": {"latitude": latitude, "longitude": longitude},
                    "walkieTalkies": [
                        {
                            "model": model,
                            "generalCommentary": to_base64(general_commentary),
                            "ranges": [
                                {
                                    "mod": mod,
                                    "min": rangemin,
                                    "max": rangemax,
                                    "commentary": to_base64(commentary),
                                }
                            ],
                        }
                    ],
                }
            ]
        }
        with open("talkies.json", "w") as f:
            json.dump(json_data, f)
        trd = TalkieRangeData("talkies.json")
    return trd


def main():
    j = Person("J", "00ff00", 0.956131, 0.587348)
    t = Person("T", "ff0080", 0.943916, 0.609007)
    a = Person("A", "ff00ff", 0.961872, 0.596557)
    l = Person("L", "0000ff", 0.942723, 0.740706)

    bf888s = WalkieTalkie(
        "BF-888S",
        "This is a BF-888S",
        [Range("antenne normal", 1000, 2000, "antenne standard")],
    )
    uv5r = WalkieTalkie(
        "UV-5R",
        "This is a UV-55R",
        [Range("antenne normal", 5000, 7000, "antenne standard")],
    )
    thuv88 = WalkieTalkie(
        "TH-UV88",
        "This is a TH-UV88",
        [
            Range("antenne normal", 3000, 5000, "antenne standard"),
            Range("antenne longue", 6000, 13000, "antenne longue"),
        ],
    )

    # we all have a BF-888S
    j.add_walkie_talkie2(bf888s)
    t.add_walkie_talkie2(bf888s)
    a.add_walkie_talkie2(bf888s)
    l.add_walkie_talkie2(bf888s)

    j.add_walkie_talkie2(uv5r)

    l.add_walkie_talkie2(thuv88)

    # trd = load_data()
    trd = TalkieRangeData("talkies.json")
    trd.add_person2(j, overwrite=True)
    trd.add_person2(t, overwrite=True)
    trd.add_person2(a, overwrite=True)
    trd.add_person2(l, overwrite=True)

    kml = simplekml.Kml()
    # Create a folder for each person
    for person in trd.people:
        folder = kml.newfolder(name=person.name)
        for walkie_talkie in person.walkie_talkies:
            # Create a folder for each walkie talkie
            wt_folder = folder.newfolder(
                name=walkie_talkie.model,
                description=from_base64(walkie_talkie.generalCommentary),
            )
            for mod in walkie_talkie.ranges:
                # Create a folder for each mod
                mod_folder = wt_folder.newfolder(
                    name=mod.mod, description=from_base64(mod.commentary)
                )

                # create two series of points for the ranges
                polygon = polycircles.Polycircle(
                    latitude=person.location["latitude"],
                    longitude=person.location["longitude"],
                    radius=mod.minrange,
                    number_of_vertices=100,
                )

                polygon2 = polycircles.Polycircle(
                    latitude=person.location["latitude"],
                    longitude=person.location["longitude"],
                    radius=mod.maxrange,
                    number_of_vertices=100,
                )

                # add the points to the kml using polygons in order
                bottomcircle = mod_folder.newpolygon(
                    name="{}-{}-{} max range".format(
                        person.name, walkie_talkie.model, mod.mod
                    ),
                    outerboundaryis=polygon2.to_kml(),
                )

                topcircle = mod_folder.newpolygon(
                    name="{}-{}-{} min range".format(
                        person.name, walkie_talkie.model, mod.mod
                    ),
                    outerboundaryis=polygon.to_kml(),
                )

                # set the color of the polygons

                bottomcircle.style.labelstyle.scale = 1
                bottomcircle.style.labelstyle.color = simplekml.Color.hexa(
                    person.color + "33"
                )
                bottomcircle.style.polystyle.color = simplekml.Color.hexa(
                    person.color + "33"
                )

                topcircle.style.labelstyle.scale = 1
                topcircle.style.labelstyle.color = simplekml.Color.hexa(
                    person.color + "80"
                )
                topcircle.style.polystyle.color = simplekml.Color.hexa(
                    person.color + "80"
                )

    kml.save("talkies.kml")


if __name__ == "__main__":
    main()


I'm very new to python oop and here is the issue I fall onto. My code create a json file the format seems to be correct but it gave all my Person the same talkies so it look like this:

  • j
    • bf888s
    • bf888s
    • bf888s
    • bf888s
    • uv5r
    • thuv88
  • l
    • bf888s
    • bf888s
    • bf888s
    • bf888s
    • uv5r
    • thuv88
  • a
    • bf888s
    • bf888s
    • bf888s
    • bf888s
    • uv5r
    • thuv88
  • t
    • bf888s
    • bf888s
    • bf888s
    • bf888s
    • uv5r
    • thuv88

By running in debug mod I found out that each time I call add_walkie_talkie2(wt) it appends the wt to all my Person, does anyone knows why this occurs ?

lolozen
  • 386
  • 1
  • 4
  • 19
  • 2
    you're making the classic mistake of using a mutable as default argument – Copperfield Dec 27 '22 at 23:15
  • could it be what causes my issue ? – lolozen Dec 28 '22 at 00:04
  • 1
    yes, that is source of the problem you describe, in your Person class you have a `walkie_talkies=list()` in its init, witch you then assign unchanged to your attribute instance, default values are created only once at class/function creation time and stored in a special dictionary within the internal of said class/function, so when the call it and don't provide a value it check in that dictionary and take it if available, thus every instance of your class for which you don't provide an explicit new value for that attribute end up sharing that same list – Copperfield Dec 28 '22 at 02:39
  • 1
    and when you append stuff to it in one instance, every other instance also see that change because all of then use the same list, that is why you see 4 copies of bf888s, everybody have uv5r and thuv88, all of then use the same list... – Copperfield Dec 28 '22 at 02:42
  • 1
    for stuff like this is that you should not use mutable object (like a list) as default values, use a either a tuple or None and handle accordingly, with None you can check if the value of the argument is None and assign a new list to the attribute, with a tuple you can do a simple call to list onto it when you assigned to the attribute `self.walkie_talkies = list(walkie_talkies)` – Copperfield Dec 28 '22 at 02:54

0 Answers0