From 871f742baa667a14cb593c812ef441f150ccf1ab Mon Sep 17 00:00:00 2001 From: baseplate-admin <61817579+baseplate-admin@users.noreply.github.com> Date: Fri, 23 Feb 2024 20:04:00 +0600 Subject: [PATCH] Work on automatic label generation (#6) * add * Update paths.py * lets see * Revert "lets see" This reverts commit 7633faceb21d1d926804b445597fbf6af1e77715. * Revert "Revert "lets see"" This reverts commit 5238e6af9eda675747e5819b0db70eff1c24341f. * Update paths.py * Update paths.py * add * Update guess_label_size.py add Update guess_label_size.py Update guess_label_size.py Update guess_label_size.py add * Update guess_label_size.py add Update guess_label_size.py Update guess_label_size.py Update guess_label_size.py add * Update paths.py * Update paths.py * Update paths.py * Update paths.py * Update paths.py * Update paths.py * Update paths.py Update paths.py * Update paths.py * Update paths.py * Update paths.py * add add Update test_label_generation.py --- django_ltree/managers.py | 1 - django_ltree/paths.py | 41 +++++- poetry.lock | 2 +- tests/taxonomy/models.py | 4 +- tests/test_label_generation.py | 13 ++ tests/test_limits.py | 0 tests/test_model.py | 256 +++++++++++++++++++-------------- tests/test_path_value.py | 16 +-- 8 files changed, 211 insertions(+), 122 deletions(-) create mode 100644 tests/test_label_generation.py create mode 100644 tests/test_limits.py diff --git a/django_ltree/managers.py b/django_ltree/managers.py index 4ef4c9b..3d67d44 100644 --- a/django_ltree/managers.py +++ b/django_ltree/managers.py @@ -27,7 +27,6 @@ def create_child(self, parent=None, **kwargs): path_generator = PathGenerator( prefix, skip=paths_in_use.values_list("path", flat=True), - label_size=getattr(self.model, "label_size"), ) kwargs["path"] = path_generator.next() return self.create(**kwargs) diff --git a/django_ltree/paths.py b/django_ltree/paths.py index e40ec06..1eeadef 100644 --- a/django_ltree/paths.py +++ b/django_ltree/paths.py @@ -1,18 +1,21 @@ import string +import math from itertools import product -from django_ltree.fields import PathValue +from .fields import PathValue class PathGenerator(object): - _default_label_size = 6 # Postgres limits this to 256 + def __init__(self, prefix=None, skip=None): + combinations = string.digits + string.ascii_letters - def __init__(self, prefix=None, skip=None, label_size=None): self.skip_paths = [] if skip is None else skip[:] self.path_prefix = prefix if prefix else [] self.product_iterator = product( - string.digits + string.ascii_letters, - repeat=label_size or self._default_label_size, + combinations, + repeat=self.guess_the_label_size( + path_size=len(self.skip_paths), combination_size=len(combinations) + ), ) def __iter__(self): @@ -26,3 +29,31 @@ def __next__(self): return path next = __next__ + + @staticmethod + def guess_the_label_size(path_size: int, combination_size: int) -> int: + # The theoritical limit for this at the time of writing is 2_538_557_185_841_324_496 (python 3.12.2) + calculated_path_size = -1 # -1 is here for 0th index items + # The theoritical limit for this at the time of writing is 32 (python 3.12.2) + label_size = 0 + + # THIS IS AN VERY IMPORTANT CHECK + last = 0 + + while True: + possible_cominations = math.comb(combination_size, label_size) + if last > possible_cominations: + raise ValueError("We approached the limit of `math.comb`") + + last = possible_cominations + calculated_path_size += possible_cominations + + if calculated_path_size > path_size and label_size != 0: + break + + label_size += 1 + + return label_size + + +print(PathGenerator.guess_the_label_size(62, 62)) diff --git a/poetry.lock b/poetry.lock index d6c3444..9c64741 100644 --- a/poetry.lock +++ b/poetry.lock @@ -214,4 +214,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "2df4ca3a096027b66f0d9106b0bb156515d5977b1eafaf33876440936fd942d4" +content-hash = "514ef7d0b5620965d773ac276e61f84469a97c3eadeb4bb080519acc7b54f7fb" diff --git a/tests/taxonomy/models.py b/tests/taxonomy/models.py index 4f50d3b..52d6eed 100644 --- a/tests/taxonomy/models.py +++ b/tests/taxonomy/models.py @@ -4,9 +4,7 @@ class Taxonomy(TreeModel): - label_size = 2 - name = models.TextField() def __str__(self): - return '{}: {}'.format(self.path, self.name) + return f"{self.path}: {self.name}" diff --git a/tests/test_label_generation.py b/tests/test_label_generation.py new file mode 100644 index 0000000..ed1f991 --- /dev/null +++ b/tests/test_label_generation.py @@ -0,0 +1,13 @@ +from django_ltree.paths import PathGenerator + + +def test_label_generation(): + assert PathGenerator.guess_the_label_size(62, 62) == 2 + assert PathGenerator.guess_the_label_size(0, 62) == 1 + + +def test_automatic_name_creation(): + from taxonomy.models import Taxonomy + + for i in range(0, 1000): + Taxonomy.objects.create_child(name=i) diff --git a/tests/test_limits.py b/tests/test_limits.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_model.py b/tests/test_model.py index e8ff8a9..bee4a99 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -4,119 +4,141 @@ TEST_DATA = [ - {'name': 'Bacteria'}, - {'name': 'Plantae'}, + {"name": "Bacteria"}, + {"name": "Plantae"}, { - 'name': 'Animalia', - 'sub': [ + "name": "Animalia", + "sub": [ { - 'name': 'Chordata', - 'sub': [ + "name": "Chordata", + "sub": [ { - 'name': 'Mammalia', - 'sub': [ + "name": "Mammalia", + "sub": [ { - 'name': 'Carnivora', - 'sub': [ + "name": "Carnivora", + "sub": [ { - 'name': 'Canidae', - 'sub': [ + "name": "Canidae", + "sub": [ { - 'name': 'Canis', - 'sub': [{'name': 'Canis lupus'}, {'name': 'Canis rufus'}] + "name": "Canis", + "sub": [ + {"name": "Canis lupus"}, + {"name": "Canis rufus"}, + ], }, { - 'name': 'Urocyon', - 'sub': [{'name': 'Urocyon cinereoargenteus'}] - } - ] + "name": "Urocyon", + "sub": [ + {"name": "Urocyon cinereoargenteus"} + ], + }, + ], }, { - 'name': 'Feliformia', - 'sub': [ + "name": "Feliformia", + "sub": [ { - 'name': 'Felidae', - 'sub': [ + "name": "Felidae", + "sub": [ { - 'name': 'Felinae', - 'sub': [ + "name": "Felinae", + "sub": [ { - 'name': 'Lynx', - 'sub': [{'name': 'Lynx lynx'}, {'name': 'Lynx rufus'}] + "name": "Lynx", + "sub": [ + { + "name": "Lynx lynx" + }, + { + "name": "Lynx rufus" + }, + ], }, { - 'name': 'Puma', - 'sub': [{'name': 'Puma concolor'}] - } - ] + "name": "Puma", + "sub": [ + { + "name": "Puma concolor" + } + ], + }, + ], } - ] + ], } - ] - } - ] + ], + }, + ], }, { - 'name': 'Pilosa', - 'sub': [ + "name": "Pilosa", + "sub": [ { - 'name': 'Folivora', - 'sub': [ + "name": "Folivora", + "sub": [ { - 'name': 'Bradypodidae', - 'sub': [ + "name": "Bradypodidae", + "sub": [ { - 'name': 'Bradypus', - 'sub': [{'name': 'Bradypus tridactylus'}] + "name": "Bradypus", + "sub": [ + { + "name": "Bradypus tridactylus" + } + ], } - ] + ], } - ] + ], } - ] - } - ] + ], + }, + ], }, { - 'name': 'Reptilia', - 'sub': [ + "name": "Reptilia", + "sub": [ { - 'name': 'Squamata', - 'sub': [ + "name": "Squamata", + "sub": [ { - 'name': 'Iguania', - 'sub': [ + "name": "Iguania", + "sub": [ { - 'name': 'Agamidae', - 'sub': [ + "name": "Agamidae", + "sub": [ { - 'name': 'Pogona', - 'sub': [ - {'name': 'Pogona barbata'}, - {'name': 'Pogona minor'}, - {'name': 'Pogona vitticeps'} - ] + "name": "Pogona", + "sub": [ + {"name": "Pogona barbata"}, + {"name": "Pogona minor"}, + { + "name": "Pogona vitticeps" + }, + ], } - ] + ], } - ] + ], } - ] + ], } - ] - } - ] + ], + }, + ], } - ] - } + ], + }, ] def create_objects(objects, parent): for obj in objects: - created = Taxonomy.objects.create_child(parent, name=obj['name']) - if 'sub' in obj: - create_objects(obj['sub'], created) + created = Taxonomy.objects.create_child(parent, name=obj["name"]) + if "sub" in obj: + create_objects(obj["sub"], created) def create_test_data(): @@ -130,21 +152,22 @@ def test_create(db): def test_roots(db): create_test_data() - roots = Taxonomy.objects.roots().values_list('name', flat=True) - assert set(roots) == set(['Bacteria', 'Plantae', 'Animalia']) + roots = Taxonomy.objects.roots().values_list("name", flat=True) + assert set(roots) == set(["Bacteria", "Plantae", "Animalia"]) @pytest.mark.parametrize( - 'name, expected', [ - ('Animalia', ['Chordata']), - ('Mammalia', ['Carnivora', 'Pilosa']), - ('Reptilia', ['Squamata']), - ('Pogona', ['Pogona barbata', 'Pogona minor', 'Pogona vitticeps']) - ] + "name, expected", + [ + ("Animalia", ["Chordata"]), + ("Mammalia", ["Carnivora", "Pilosa"]), + ("Reptilia", ["Squamata"]), + ("Pogona", ["Pogona barbata", "Pogona minor", "Pogona vitticeps"]), + ], ) def test_children(db, name, expected): create_test_data() - children = Taxonomy.objects.get(name=name).children().values_list('name', flat=True) + children = Taxonomy.objects.get(name=name).children().values_list("name", flat=True) assert set(children) == set(expected) @@ -157,53 +180,78 @@ def test_label(db): @pytest.mark.parametrize( - 'name, expected', [ - ('Canis lupus', ['Animalia', 'Chordata', 'Mammalia', 'Carnivora', 'Canidae', 'Canis', 'Canis lupus']), - ('Bacteria', ['Bacteria']), - ('Chordata', ['Animalia', 'Chordata']) - ] + "name, expected", + [ + ( + "Canis lupus", + [ + "Animalia", + "Chordata", + "Mammalia", + "Carnivora", + "Canidae", + "Canis", + "Canis lupus", + ], + ), + ("Bacteria", ["Bacteria"]), + ("Chordata", ["Animalia", "Chordata"]), + ], ) def test_ancestors(db, name, expected): create_test_data() - ancestors = Taxonomy.objects.get(name=name).ancestors().values_list('name', flat=True) + ancestors = ( + Taxonomy.objects.get(name=name).ancestors().values_list("name", flat=True) + ) assert list(ancestors) == expected @pytest.mark.parametrize( - 'name, expected', [ - ('Canidae', ['Canidae', 'Canis', 'Canis lupus', 'Canis rufus', 'Urocyon', 'Urocyon cinereoargenteus']), - ('Bradypus tridactylus', ['Bradypus tridactylus']), - ('Pogona', ['Pogona', 'Pogona barbata', 'Pogona minor', 'Pogona vitticeps']) - ] + "name, expected", + [ + ( + "Canidae", + [ + "Canidae", + "Canis", + "Canis lupus", + "Canis rufus", + "Urocyon", + "Urocyon cinereoargenteus", + ], + ), + ("Bradypus tridactylus", ["Bradypus tridactylus"]), + ("Pogona", ["Pogona", "Pogona barbata", "Pogona minor", "Pogona vitticeps"]), + ], ) def test_descendants(db, name, expected): create_test_data() - descendants = Taxonomy.objects.get(name=name).descendants().values_list('name', flat=True) + descendants = ( + Taxonomy.objects.get(name=name).descendants().values_list("name", flat=True) + ) assert set(descendants) == set(expected) @pytest.mark.parametrize( - 'name, expected', [ - ('Feliformia', 'Carnivora'), - ('Plantae', None), - ('Pogona minor', 'Pogona') - ] + "name, expected", + [("Feliformia", "Carnivora"), ("Plantae", None), ("Pogona minor", "Pogona")], ) def test_parent(db, name, expected): create_test_data() parent = Taxonomy.objects.get(name=name).parent() - assert getattr(parent, 'name', None) == expected + assert getattr(parent, "name", None) == expected @pytest.mark.parametrize( - 'name, expected', [ - ('Carnivora', ['Pilosa']), - ('Pogona vitticeps', ['Pogona minor', 'Pogona barbata']) - ] + "name, expected", + [ + ("Carnivora", ["Pilosa"]), + ("Pogona vitticeps", ["Pogona minor", "Pogona barbata"]), + ], ) def test_siblings(db, name, expected): create_test_data() - siblings = Taxonomy.objects.get(name=name).siblings().values_list('name', flat=True) + siblings = Taxonomy.objects.get(name=name).siblings().values_list("name", flat=True) assert set(siblings) == set(expected) diff --git a/tests/test_path_value.py b/tests/test_path_value.py index 7708714..acc4543 100644 --- a/tests/test_path_value.py +++ b/tests/test_path_value.py @@ -4,15 +4,15 @@ def test_create(): - assert str(PathValue([1, 2, 3, 4, 5])) == '1.2.3.4.5' - assert str(PathValue((1, 3, 5, 7))) == '1.3.5.7' - assert str(PathValue('hello.world')) == 'hello.world' + assert str(PathValue([1, 2, 3, 4, 5])) == "1.2.3.4.5" + assert str(PathValue((1, 3, 5, 7))) == "1.3.5.7" + assert str(PathValue("hello.world")) == "hello.world" assert str(PathValue(5)) == "5" def generator(): - yield '100' - yield 'bottles' - yield 'of' - yield 'beer' + yield "100" + yield "bottles" + yield "of" + yield "beer" - assert str(PathValue(generator())) == '100.bottles.of.beer' + assert str(PathValue(generator())) == "100.bottles.of.beer"