From: Hans-Christoph Steiner Date: Wed, 23 Nov 2016 16:25:59 +0000 (+0100) Subject: convert App to subclass of dict to support parsing/dumping libs X-Git-Tag: 0.8~121^2~7 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=commitdiff_plain;h=b7fc7f2228986d0210e221c9ec8ddcc2ad9b93bc;p=fdroidserver.git convert App to subclass of dict to support parsing/dumping libs Python is heavily based on its core data types, and dict is one of the more important ones. Even classes are basically a wrapper around a dict. This converts metadata.App to be a subclass of dict so it can behave like a dict when being dumped and loaded. This makes its drastically easier to use different data formats for build metadata and for sending data to the client. This approach will ultimately mean we no longer have to maintain custom parsing and dumping code. This also means then that the YAML/JSON field names will not have spaces in them, and they will match exactly what it used as the dict keys once the data is parsed, as well as matching exactly the instance attribute names: * CurrentVersion: 1.2.6 * app['CurrentVersion'] == '1.2.6' * app.CurrentVersion == '1.2.6' Inspired by: https://goodcode.io/articles/python-dict-object/ --- diff --git a/fdroidserver/lint.py b/fdroidserver/lint.py index fb35d9cb..f70097c4 100644 --- a/fdroidserver/lint.py +++ b/fdroidserver/lint.py @@ -63,10 +63,10 @@ http_checks = https_enforcings + http_url_shorteners + [ ] regex_checks = { - 'Web Site': http_checks, - 'Source Code': http_checks, + 'WebSite': http_checks, + 'SourceCode': http_checks, 'Repo': https_enforcings, - 'Issue Tracker': http_checks + [ + 'IssueTracker': http_checks + [ (re.compile(r'.*github\.com/[^/]+/[^/]+/*$'), "/issues is missing"), (re.compile(r'.*gitlab\.com/[^/]+/[^/]+/*$'), @@ -121,7 +121,7 @@ regex_checks = { def check_regexes(app): for f, checks in regex_checks.items(): for m, r in checks: - v = app.get_field(f) + v = app.get(f) t = metadata.fieldtype(f) if t == metadata.TYPE_MULTILINE: for l in v.splitlines(): @@ -183,8 +183,8 @@ def check_old_links(app): 'code.google.com', ] if any(s in app.Repo for s in usual_sites): - for f in ['Web Site', 'Source Code', 'Issue Tracker', 'Changelog']: - v = app.get_field(f) + for f in ['WebSite', 'SourceCode', 'IssueTracker', 'Changelog']: + v = app.get(f) if any(s in v for s in old_sites): yield "App is in '%s' but has a link to '%s'" % (app.Repo, v) @@ -241,7 +241,7 @@ def check_duplicates(app): links_seen = set() for f in ['Source Code', 'Web Site', 'Issue Tracker', 'Changelog']: - v = app.get_field(f) + v = app.get(f) if not v: continue v = v.lower() diff --git a/fdroidserver/metadata.py b/fdroidserver/metadata.py index 6fb69a48..2b091e2c 100644 --- a/fdroidserver/metadata.py +++ b/fdroidserver/metadata.py @@ -98,15 +98,21 @@ app_fields = set([ 'Current Version', 'Current Version Code', 'No Source Since', + 'Build', 'comments', # For formats that don't do inline comments 'builds', # For formats that do builds as a list ]) -class App(): +class App(dict): + + def __init__(self, copydict=None): + if copydict: + super().__init__(copydict) + return + super().__init__() - def __init__(self): self.Disabled = None self.AntiFeatures = [] self.Provides = None @@ -148,94 +154,21 @@ class App(): self.comments = {} self.added = None self.lastupdated = None - self._modified = set() - - @classmethod - def field_to_attr(cls, f): - """ - Translates human-readable field names to attribute names, e.g. - 'Auto Name' to 'AutoName' - """ - return f.replace(' ', '') - - @classmethod - def attr_to_field(cls, k): - """ - Translates attribute names to human-readable field names, e.g. - 'AutoName' to 'Auto Name' - """ - if k in app_fields: - return k - f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k) - return f - def field_dict(self): - """ - Constructs an old-fashioned dict with the human-readable field - names. Should only be used for tests. - """ - d = {} - for k, v in self.__dict__.items(): - if k == 'builds': - d['builds'] = [] - for build in v: - b = {k: v for k, v in build.__dict__.items() if not k.startswith('_')} - d['builds'].append(b) - elif not k.startswith('_'): - f = App.attr_to_field(k) - d[f] = v - return d - - def get_field(self, f): - """Gets the value associated to a field name, e.g. 'Auto Name'""" - if f not in app_fields: - warn_or_exception('Unrecognised app field: ' + f) - k = App.field_to_attr(f) - return getattr(self, k) - - def set_field(self, f, v): - """Sets the value associated to a field name, e.g. 'Auto Name'""" - if f not in app_fields: - warn_or_exception('Unrecognised app field: ' + f) - k = App.field_to_attr(f) - self.__dict__[k] = v - self._modified.add(k) - - def append_field(self, f, v): - """Appends to the value associated to a field name, e.g. 'Auto Name'""" - if f not in app_fields: - warn_or_exception('Unrecognised app field: ' + f) - k = App.field_to_attr(f) - if k not in self.__dict__: - self.__dict__[k] = [v] + def __getattr__(self, name): + if name in self: + return self[name] else: - self.__dict__[k].append(v) + raise AttributeError("No such attribute: " + name) - def update_fields(self, d): - '''Like dict.update(), but using human-readable field names''' - for f, v in d.items(): - if f == 'builds': - for b in v: - build = Build() - build.update_flags(b) - self.builds.append(build) - else: - self.set_field(f, v) + def __setattr__(self, name, value): + self[name] = value - def update(self, d): - '''Like dict.update()''' - for k, v in d.__dict__.items(): - if k == '_modified': - continue - elif k == 'builds': - for b in v: - build = Build() - del(b.__dict__['_modified']) - build.update_flags(b.__dict__) - self.builds.append(build) - elif v: - self.__dict__[k] = v - self._modified.add(k) + def __delattr__(self, name): + if name in self: + del self[name] + else: + raise AttributeError("No such attribute: " + name) def get_last_build(self): if len(self.builds) > 0: @@ -256,16 +189,17 @@ TYPE_BUILD_V2 = 8 fieldtypes = { 'Description': TYPE_MULTILINE, - 'Maintainer Notes': TYPE_MULTILINE, + 'MaintainerNotes': TYPE_MULTILINE, 'Categories': TYPE_LIST, 'AntiFeatures': TYPE_LIST, - 'Build Version': TYPE_BUILD, + 'BuildVersion': TYPE_BUILD, 'Build': TYPE_BUILD_V2, - 'Use Built': TYPE_OBSOLETE, + 'UseBuilt': TYPE_OBSOLETE, } def fieldtype(name): + name = name.replace(' ', '') if name in fieldtypes: return fieldtypes[name] return TYPE_STRING @@ -518,9 +452,7 @@ valuetypes = { def check_metadata(app): for v in valuetypes: for k in v.fields: - if k not in app._modified: - continue - v.check(app.__dict__[k], app.id) + v.check(app[k], app.id) # Formatter for descriptions. Create an instance, and call parseline() with @@ -896,44 +828,21 @@ def sorted_builds(builds): esc_newlines = re.compile(r'\\( |\n)') -# This function uses __dict__ to be faster def post_metadata_parse(app): - - for k in app._modified: - v = app.__dict__[k] + # TODO keep native types, convert only for .txt metadata + for k, v in app.items(): if type(v) in (float, int): - app.__dict__[k] = str(v) + app[k] = str(v) builds = [] - for build in app.builds: - if not isinstance(build, Build): - build = Build(build) - builds.append(build) - - for k in build._modified: - v = build.__dict__[k] - if type(v) in (float, int): - build.__dict__[k] = str(v) - continue - ftype = flagtype(k) - - if ftype == TYPE_SCRIPT: - build.__dict__[k] = re.sub(esc_newlines, '', v).lstrip().rstrip() - elif ftype == TYPE_BOOL: - # TODO handle this using