From 1b9b65ed8cf029f575459e3c05342698ce7c0e5e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 23 Jun 2020 21:36:11 +0100 Subject: [PATCH] Don't subclass RecData for RecipeManager * parse_ingredient() method belongs to RecipeManager, not RecData * Standardise not-quite-singleton machinery for RecData & RecipeManager See kirienko/gourmet PR #125 --- gourmet/backends/db.py | 106 +++++++++++++++------- gourmet/importers/interactive_importer.py | 21 ++--- gourmet/recipeManager.py | 13 +-- 3 files changed, 86 insertions(+), 54 deletions(-) diff --git a/gourmet/backends/db.py b/gourmet/backends/db.py index 4bc49bef5..c678bc3db 100644 --- a/gourmet/backends/db.py +++ b/gourmet/backends/db.py @@ -114,7 +114,15 @@ class DBObject: # categories_table: id -> recipe_id, category_entry_id -> id # ingredients_table: ingredient_id -> id, id -> recipe_id -class RecData (Pluggable, BaseException): +def db_url(file: Optional[str]=None, custom_url: Optional[str]=None) -> str: + if custom_url is not None: + return custom_url + else: + if file is None: + file = os.path.join(gglobals.gourmetdir, 'recipes.db') + return 'sqlite:///' + file + +class RecData (Pluggable): """RecData is our base class for handling database connections. @@ -125,27 +133,29 @@ class RecData (Pluggable, BaseException): AMT_MODE_AVERAGE = 1 AMT_MODE_HIGH = 2 - _singleton = {} + _instance_by_db_url = {} + + @classmethod + def instance_for( + cls, file: Optional[str]=None, custom_url: Optional[str]=None + ) -> 'RecData': + url = db_url(file, custom_url) + + if url not in cls._instance_by_db_url: + cls._instance_by_db_url[url] = cls(file, url) - def __init__ (self, file=os.path.join(gglobals.gourmetdir,'recipes.db'), - custom_url=None): + return cls._instance_by_db_url[url] + + def __init__ (self, file: str, url: str): # hooks run after adding, modifying or deleting a recipe. # Each hook is handed the recipe, except for delete_hooks, # which is handed the ID (since the recipe has been deleted) - if file in RecData._singleton: - raise RecData._singleton[file] - else: - RecData._singleton[file] = self # We keep track of IDs we've handed out with new_id() in order # to prevent collisions self.new_ids = [] self._created = False - if custom_url: - self.url = custom_url - self.filename = None - else: - self.filename = file - self.url = 'sqlite:///' + self.filename + self.filename = file + self.url = url self.add_hooks = [] self.modify_hooks = [] self.delete_hooks = [] @@ -1843,14 +1853,37 @@ def get_default_values (self, colname): return [] -class RecipeManager (RecData): +class RecipeManager: + _instance_by_db_url = {} - def __init__ (self,*args,**kwargs): + @classmethod + def instance_for( + cls, file: Optional[str]=None, custom_url: Optional[str]=None + ) -> 'RecipeManager': + url = db_url(file, custom_url) + + if url not in cls._instance_by_db_url: + cls._instance_by_db_url[url] = cls(file, custom_url) + + return cls._instance_by_db_url[url] + + def __init__ (self, *args, **kwargs): debug('recipeManager.__init__()',3) - RecData.__init__(self,*args,**kwargs) + self.rd = get_database(*args, **kwargs) #self.km = keymanager.KeyManager(rm=self) self.km = keymanager.get_keymanager(rm=self) + def __getattr__(self, name): + # RecipeManager was previously a subclass of RecData. + # This was changed as they're both used as singletons, and there's + # no good way to have a subclassed singleton (unless the parent class + # is an abstract thing that's never used directly, which it wasn't). + # However, lots of code uses RecData methods on RecipeManager objects. + # This ensures that that code keeps working. + if name.startswith('_'): + raise AttributeError(name) + return getattr(self.rd, name) + def key_search (self, ing): """Handed a string, we search for keys that could match the ingredient.""" @@ -1907,7 +1940,7 @@ def parse_ingredient (self, s, conv=None, get_key=True): d['unit']=u.strip() else: # has this unit been used - prev_uses = self.fetch_all(self.ingredients_table,unit=u.strip()) + prev_uses = self.rd.fetch_all(self.rd.ingredients_table,unit=u.strip()) if prev_uses: d['unit']=u else: @@ -1931,10 +1964,25 @@ def parse_ingredient (self, s, conv=None, get_key=True): def ing_search (self, ing, keyed=None, recipe_table=None, use_regexp=True, exact=False): """Search for an ingredient.""" - if not recipe_table: recipe_table = self.recipe_table - vw = self.joined_search(recipe_table,self.ingredients_table,'ingkey',ing,use_regexp=use_regexp,exact=exact) + if not recipe_table: + recipe_table = self.rd.recipe_table + vw = self.joined_search( + recipe_table, + self.rd.ingredients_table, + search_by='ingkey', + search_str=ing, + use_regexp=use_regexp, + exact=exact + ) if not keyed: - vw2 = self.joined_search(recipe_table,self.ingredients_table,'item',ing,use_regexp=use_regexp,exact=exact) + vw2 = self.joined_search( + recipe_table, + self.rd.ingredients_table, + search_by='item', + search_str=ing, + use_regexp=use_regexp, + exact=exact + ) if vw2 and vw: vw = vw.union(vw2) else: vw = vw2 @@ -1956,14 +2004,14 @@ def clear_remembered_optional_ings (self, recipe=None): Otherwise, we clear *all* recipes. """ if recipe: - vw = self.get_ings(recipe) + vw = self.rd.get_ings(recipe) else: - vw = self.ingredients_table + vw = self.rd.ingredients_table # this is ugly... vw1 = vw.select(shopoptional=1) vw2 = vw.select(shopoptional=2) for v in vw1,vw2: - for i in v: self.modify_ing(i,{'shopoptional':0}) + for i in v: self.rd.modify_ing(i,{'shopoptional':0}) class DatabaseConverter(convert.Converter): def __init__ (self, db): @@ -2098,11 +2146,5 @@ def items (self): # fetch_all -> #recipe_table -> recipe_table -def get_database (*args,**kwargs): - try: - return RecData(*args,**kwargs) - except RecData as rd: - return rd - -if __name__ == '__main__': - db = RecData() +def get_database (*args, **kwargs): + return RecData.instance_for(*args, **kwargs) diff --git a/gourmet/importers/interactive_importer.py b/gourmet/importers/interactive_importer.py index 9f8e5e89c..7fc8d85b9 100644 --- a/gourmet/importers/interactive_importer.py +++ b/gourmet/importers/interactive_importer.py @@ -1,14 +1,16 @@ -from gi.repository import Gtk, Pango +from gettext import gettext as _ +import re from xml.sax.saxutils import escape -from .generic_recipe_parser import RecipeParser + +from gi.repository import Gtk, Pango + import gourmet.gtk_extras.cb_extras as cb import gourmet.gglobals as gglobals -from . import importer -import re -from gourmet.threadManager import NotThreadSafe -from . import imageBrowser import gourmet.ImageExtras as ImageExtras -from gettext import gettext as _ +from gourmet.recipeManager import get_recipe_manager +from gourmet.threadManager import NotThreadSafe +from . import importer, imageBrowser +from .generic_recipe_parser import RecipeParser # TODO # 1. Make this interface actually import recipes... @@ -75,10 +77,7 @@ def add_ing_group (self, txt): self.group = txt.strip() def add_ing_from_text (self, txt): - if not hasattr(self,'db'): - import gourmet.backends.db as db - self.db = db.get_database() - parsed_dict = self.db.parse_ingredient(txt) + parsed_dict = get_recipe_manager().parse_ingredient(txt) self.ing = parsed_dict self.commit_ing() diff --git a/gourmet/recipeManager.py b/gourmet/recipeManager.py index f970dc35b..7575cdc36 100644 --- a/gourmet/recipeManager.py +++ b/gourmet/recipeManager.py @@ -78,20 +78,11 @@ class through self.rm. except: print('invalid input.') -def get_recipe_manager (**args): - if not args: args = dbargs - try: - return RecipeManager(**args) - except RecData as rd: - return rd +def get_recipe_manager (**kwargs): + return RecipeManager.instance_for(**kwargs) def default_rec_manager (): return get_recipe_manager(**dbargs) - #try: - # return RecipeManager(**dbargs) - #except RecData,rd: - # return rd - if __name__ == '__main__': #rm = RecipeManager(**dbargs)