From edf3c7aac70a29157340f671a3ebf3372273354f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 20 Apr 2018 15:59:35 -0400 Subject: [PATCH 01/21] reformat to fit within 80 cols --- src/allmydata/webish.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index 416e34dfd..5e52dbd28 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -114,16 +114,20 @@ class MyRequest(appserver.NevowRequest): if self._tahoe_request_had_error: error = " [ERROR]" - log.msg(format="web: %(clientip)s %(method)s %(uri)s %(code)s %(length)s%(error)s", - clientip=self.getClientIP(), - method=self.method, - uri=uri, - code=self.code, - length=(self.sentLength or "-"), - error=error, - facility="tahoe.webish", - level=log.OPERATIONAL, - ) + log.msg( + format=( + "web: %(clientip)s %(method)s %(uri)s %(code)s " + "%(length)s%(error)s" + ), + clientip=self.getClientIP(), + method=self.method, + uri=uri, + code=self.code, + length=(self.sentLength or "-"), + error=error, + facility="tahoe.webish", + level=log.OPERATIONAL, + ) class WebishServer(service.MultiService): @@ -218,4 +222,3 @@ class IntroducerWebishServer(WebishServer): service.MultiService.__init__(self) self.root = introweb.IntroducerRoot(introducer) self.buildServer(webport, nodeurl_path, staticdir) - From 8eb83bbfb98653a22ad676be9450b818db81ff42 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 20 Apr 2018 16:03:19 -0400 Subject: [PATCH 02/21] avoid about-to-be-deprecated getClientIP if we can Use the replacement, getClientAddress. But have a fallback to getClientIP to keep supporting older versions of Twisted. --- src/allmydata/webish.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index 5e52dbd28..d1a60f495 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -2,6 +2,10 @@ import re, time from twisted.application import service, strports, internet from twisted.web import http, static from twisted.internet import defer +from twisted.internet.address import ( + IPv4Address, + IPv6Address, +) from nevow import appserver, inevow from allmydata.util import log, fileutil @@ -119,7 +123,7 @@ class MyRequest(appserver.NevowRequest): "web: %(clientip)s %(method)s %(uri)s %(code)s " "%(length)s%(error)s" ), - clientip=self.getClientIP(), + clientip=_get_client_ip(self), method=self.method, uri=uri, code=self.code, @@ -130,6 +134,18 @@ class MyRequest(appserver.NevowRequest): ) +def _get_client_ip(request): + try: + get = request.getClientAddress + except AttributeError: + return request.getClientIP() + else: + client_addr = get() + if isinstance(client_addr, (IPv4Address, IPv6Address)): + return client_addr.host + return None + + class WebishServer(service.MultiService): name = "webish" From 2bc4aa7d14d7253182caa366716b59d8dc989c75 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Apr 2018 09:04:45 -0400 Subject: [PATCH 03/21] Add recommended lgtm configuration. --- .lgtm.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .lgtm.yml diff --git a/.lgtm.yml b/.lgtm.yml new file mode 100644 index 000000000..0e0f0db13 --- /dev/null +++ b/.lgtm.yml @@ -0,0 +1,6 @@ +extraction: + python: + after_prepare: + - | + # https://discuss.lgtm.com/t/determination-of-python-requirements/974/4 + sed -i 's/\("pyOpenSSL\)/\# Dependency removed for lgtm (see .lgtm.yml): \1/g' src/allmydata/_auto_deps.py From be0317632fc47a79bc77a22c6bf7de21b58c876b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 14:50:25 -0400 Subject: [PATCH 04/21] Turn off a query that generates spurious errors --- .lgtm.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.lgtm.yml b/.lgtm.yml index 0e0f0db13..240bc62c5 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -4,3 +4,9 @@ extraction: - | # https://discuss.lgtm.com/t/determination-of-python-requirements/974/4 sed -i 's/\("pyOpenSSL\)/\# Dependency removed for lgtm (see .lgtm.yml): \1/g' src/allmydata/_auto_deps.py + +queries: + # This generates spurious errors for calls by interface because of the + # zope.interface choice to exclude self from method signatures. So, turn it + # off. + - exclude: "py/call/wrong-arguments" From cc6006dcb3695531582c3c1e466598a0184281da Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 14:57:45 -0400 Subject: [PATCH 05/21] Replace use of deprecated sha module --- misc/simulators/simulator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/simulators/simulator.py b/misc/simulators/simulator.py index c1f9e9b54..0cf568eaf 100644 --- a/misc/simulators/simulator.py +++ b/misc/simulators/simulator.py @@ -1,6 +1,6 @@ #! /usr/bin/env python -import sha as shamodule +import hashlib import os, random from pkg_resources import require @@ -10,7 +10,7 @@ from pyrrd.rrd import DataSource, RRD, RRA def sha(s): - return shamodule.new(s).digest() + return hashlib.sha1(s).digest() def randomid(): return os.urandom(20) From f99b3bdbda4ee89caa3186dcc13f3102063d1805 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 14:58:40 -0400 Subject: [PATCH 06/21] Remove complicated and dead code --- misc/simulators/simulator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/misc/simulators/simulator.py b/misc/simulators/simulator.py index 0cf568eaf..a01c5c8c8 100644 --- a/misc/simulators/simulator.py +++ b/misc/simulators/simulator.py @@ -70,10 +70,7 @@ class Node: return False def decide(self, sharesize): - if sharesize > self.capacity: - return False return False - return random.random() > 0.5 def make_space(self, sharesize): assert sharesize <= self.capacity From da9d0ded94eabba0c42d1a332cf2e3cfc6f9f004 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 14:59:18 -0400 Subject: [PATCH 07/21] Remove pointless conditional --- src/allmydata/mutable/publish.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/allmydata/mutable/publish.py b/src/allmydata/mutable/publish.py index ee17847e4..fe01efa01 100644 --- a/src/allmydata/mutable/publish.py +++ b/src/allmydata/mutable/publish.py @@ -976,8 +976,7 @@ class Publish: i += 1 if i >= len(serverlist): i = 0 - if True: - self.log_goal(self.goal, "after update: ") + self.log_goal(self.goal, "after update: ") def _got_write_answer(self, answer, writer, started): From 6d9f0c59b7bfcd2cd52d6a1a866a76fc93f50382 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 14:59:47 -0400 Subject: [PATCH 08/21] Remove pointless conditional --- src/allmydata/mutable/publish.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/allmydata/mutable/publish.py b/src/allmydata/mutable/publish.py index fe01efa01..50db1e2e3 100644 --- a/src/allmydata/mutable/publish.py +++ b/src/allmydata/mutable/publish.py @@ -909,9 +909,7 @@ class Publish: level=log.NOISY) def update_goal(self): - # if log.recording_noisy - if True: - self.log_goal(self.goal, "before update: ") + self.log_goal(self.goal, "before update: ") # first, remove any bad servers from our goal self.goal = set([ (server, shnum) From 206ab732e6fa306af6cf19d6cc0d78f8dc42587a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:00:36 -0400 Subject: [PATCH 09/21] Replace use of deprecated sha module --- misc/coding_tools/make-canary-files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/coding_tools/make-canary-files.py b/misc/coding_tools/make-canary-files.py index 57f900a5c..4ba06cd9c 100644 --- a/misc/coding_tools/make-canary-files.py +++ b/misc/coding_tools/make-canary-files.py @@ -50,7 +50,7 @@ system where Tahoe is installed, or in a source tree with setup.py like this: setup.py run_with_pythonpath -p -c 'misc/make-canary-files.py ARGS..' """ -import os, sha +import os, hashlib from twisted.python import usage from allmydata.immutable import upload from allmydata.util import base32 @@ -96,7 +96,7 @@ convergence = base32.a2b(convergence_s) def get_permuted_peers(key): results = [] for nodeid in nodes: - permuted = sha.new(key + nodeid).digest() + permuted = hashlib.sha1(key + nodeid).digest() results.append((permuted, nodeid)) results.sort(lambda a,b: cmp(a[0], b[0])) return [ r[1] for r in results ] From 0f5d0e3131b814bbcf80683797503fad1af1ae7b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:07:09 -0400 Subject: [PATCH 10/21] Comment out "testing" code... --- misc/operations_helpers/munin/tahoe_nodememory | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/misc/operations_helpers/munin/tahoe_nodememory b/misc/operations_helpers/munin/tahoe_nodememory index fd3f8b0a4..1ecf53fc9 100644 --- a/misc/operations_helpers/munin/tahoe_nodememory +++ b/misc/operations_helpers/munin/tahoe_nodememory @@ -6,10 +6,9 @@ import os, sys, re -if 0: - # for testing - os.environ["nodememory_warner1"] = "run/warner1" - os.environ["nodememory_warner2"] = "run/warner2" +# for testing +# os.environ["nodememory_warner1"] = "run/warner1" +# os.environ["nodememory_warner2"] = "run/warner2" nodedirs = [] for k,v in os.environ.items(): From 64243527eb112e2d7d5c436b64dbd3ef5d442c76 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:08:14 -0400 Subject: [PATCH 11/21] Remove the strange option to not use flog --- src/allmydata/frontends/sftpd.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/allmydata/frontends/sftpd.py b/src/allmydata/frontends/sftpd.py index 028ea7ee5..83d1e540a 100644 --- a/src/allmydata/frontends/sftpd.py +++ b/src/allmydata/frontends/sftpd.py @@ -38,24 +38,9 @@ from allmydata.dirnode import update_metadata from allmydata.util.fileutil import EncryptedTemporaryFile noisy = True -use_foolscap_logging = True from allmydata.util.log import NOISY, OPERATIONAL, WEIRD, \ - msg as _msg, err as _err, PrefixingLogMixin as _PrefixingLogMixin - -if use_foolscap_logging: - (logmsg, logerr, PrefixingLogMixin) = (_msg, _err, _PrefixingLogMixin) -else: # pragma: no cover - def logmsg(s, level=None): - print s - def logerr(s, level=None): - print s - class PrefixingLogMixin: - def __init__(self, facility=None, prefix=''): - self.prefix = prefix - def log(self, s, level=None): - print "%r %s" % (self.prefix, s) - + msg as logmsg, PrefixingLogMixin def eventually_callback(d): return lambda res: eventually(d.callback, res) From 7609fd18617c02043c278551fc7c7172fad90838 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:09:01 -0400 Subject: [PATCH 12/21] Remove impossible third codepath --- src/allmydata/immutable/layout.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/allmydata/immutable/layout.py b/src/allmydata/immutable/layout.py index c634702ab..320f742e0 100644 --- a/src/allmydata/immutable/layout.py +++ b/src/allmydata/immutable/layout.py @@ -438,9 +438,7 @@ class ReadBucketProxy(object): def _get_share_hashes(self, unused=None): if hasattr(self, '_share_hashes'): return self._share_hashes - else: - return self._get_share_hashes_the_old_way() - return self._share_hashes + return self._get_share_hashes_the_old_way() def get_share_hashes(self): d = self._start_if_needed() From 9f8c90393faed664a0b97ef65e761a8c31151058 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:10:02 -0400 Subject: [PATCH 13/21] Remove dead `synopsis` definition --- src/allmydata/scripts/runner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py index d616cf199..70ff75bf2 100644 --- a/src/allmydata/scripts/runner.py +++ b/src/allmydata/scripts/runner.py @@ -47,7 +47,6 @@ class Options(usage.Options): stdout = sys.stdout stderr = sys.stderr - synopsis = "\nUsage: tahoe [command options]" subCommands = ( GROUP("Administration") + create_node.subCommands + stats_gatherer.subCommands From 6b16afaa2eae926b95b6b7c325cf24e7139d93df Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:16:00 -0400 Subject: [PATCH 14/21] Avoid using the list comprehension loop variable It works fine but it relies on leaky scopes. --- src/allmydata/util/encodingutil.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/util/encodingutil.py b/src/allmydata/util/encodingutil.py index 65f5911a1..11ab942b6 100644 --- a/src/allmydata/util/encodingutil.py +++ b/src/allmydata/util/encodingutil.py @@ -335,8 +335,8 @@ def listdir_unicode_fallback(path): try: return [unicode(fn, filesystem_encoding) for fn in os.listdir(byte_path)] - except UnicodeDecodeError: - raise FilenameEncodingError(fn) + except UnicodeDecodeError as e: + raise FilenameEncodingError(e.object) def listdir_unicode(path): """ From 8d4d0001326415113b0bd35346c2a07857602d36 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:20:27 -0400 Subject: [PATCH 15/21] Fix pre-release matching regex character class Previously matched any single character from `abc|r` (with duplicate specification of `c`). Now matches any single character from `abc` or the two character sequence `rc`. I guess this was the intent, anyway. --- src/allmydata/util/verlib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/allmydata/util/verlib.py b/src/allmydata/util/verlib.py index 6d8d8e254..619f1a845 100644 --- a/src/allmydata/util/verlib.py +++ b/src/allmydata/util/verlib.py @@ -254,7 +254,7 @@ def suggest_normalized_version(s): # if we have something like "b-2" or "a.2" at the end of the # version, that is pobably beta, alpha, etc # let's remove the dash or dot - rs = re.sub(r"([abc|rc])[\-\.](\d+)$", r"\1\2", rs) + rs = re.sub(r"([abc]|rc)[\-\.](\d+)$", r"\1\2", rs) # 1.0-dev-r371 -> 1.0.dev371 # 0.1-dev-r79 -> 0.1.dev79 @@ -324,4 +324,3 @@ def suggest_normalized_version(s): except IrrationalVersionError: pass return None - From b6d33c92ff08b86cbcbad53039fa197a7c3bcaf3 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:26:17 -0400 Subject: [PATCH 16/21] Remove disabled ad hoc debug logging --- src/allmydata/web/directory.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index f21e1dffa..1cfd7a17b 100644 --- a/src/allmydata/web/directory.py +++ b/src/allmydata/web/directory.py @@ -74,8 +74,6 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): return d def got_child(self, node_or_failure, ctx, name): - DEBUG = False - if DEBUG: print "GOT_CHILD", name, node_or_failure req = IRequest(ctx) method = req.method nonterminal = len(req.postpath) > 1 @@ -84,24 +82,18 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): f = node_or_failure f.trap(NoSuchChildError) # No child by this name. What should we do about it? - if DEBUG: print "no child", name - if DEBUG: print "postpath", req.postpath if nonterminal: - if DEBUG: print " intermediate" if should_create_intermediate_directories(req): # create intermediate directories - if DEBUG: print " making intermediate directory" d = self.node.create_subdirectory(name) d.addCallback(make_handler_for, self.client, self.node, name) return d else: - if DEBUG: print " terminal" # terminal node if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir"), ("POST", "mkdir-with-children"), ("POST", "mkdir-immutable") ]: - if DEBUG: print " making final directory" # final directory kids = {} if t in ("mkdir-with-children", "mkdir-immutable"): @@ -122,14 +114,12 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): self.client, self.node, name) return d if (method,t) in ( ("PUT",""), ("PUT","uri"), ): - if DEBUG: print " PUT, making leaf placeholder" # we were trying to find the leaf filenode (to put a new # file in its place), and it didn't exist. That's ok, # since that's the leaf node that we're about to create. # We make a dummy one, which will respond to the PUT # request by replacing itself. return PlaceHolderNodeHandler(self.client, self.node, name) - if DEBUG: print " 404" # otherwise, we just return a no-such-child error return f @@ -138,11 +128,9 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): if not IDirectoryNode.providedBy(node): # we would have put a new directory here, but there was a # file in the way. - if DEBUG: print "blocking" raise WebError("Unable to create directory '%s': " "a file was in the way" % name, http.CONFLICT) - if DEBUG: print "good child" return make_handler_for(node, self.client, self.node, name) def render_DELETE(self, ctx): From 3705264740d90f244f4b6d819d22a84ac36366fb Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:27:00 -0400 Subject: [PATCH 17/21] Use preferred exception raising syntax. Also, make the `WindowsError` class "reachable". --- src/allmydata/windows/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/windows/registry.py b/src/allmydata/windows/registry.py index 2b87689b8..4801310d3 100644 --- a/src/allmydata/windows/registry.py +++ b/src/allmydata/windows/registry.py @@ -5,9 +5,9 @@ _AMD_KEY = r"Software\Allmydata" _BDIR_KEY = 'Base Dir Path' if sys.platform not in ('win32'): - raise ImportError, "registry cannot be used on non-windows systems" class WindowsError(Exception): # stupid voodoo to appease pyflakes pass + raise ImportError("registry cannot be used on non-windows systems") def get_registry_setting(key, name, _topkey=None): """ From 87daa3ec5a5113e2f790c35409214e94ab0492df Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:32:02 -0400 Subject: [PATCH 18/21] Remove dead debug logging code. --- src/allmydata/immutable/upload.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/immutable/upload.py b/src/allmydata/immutable/upload.py index cef226a8e..26d0af30d 100644 --- a/src/allmydata/immutable/upload.py +++ b/src/allmydata/immutable/upload.py @@ -533,7 +533,6 @@ class Tahoe2ServerSelector(log.PrefixingLogMixin): # we haven't improved over the last iteration; give up break; if errors_before == self._query_stats.bad: - if False: print("no more errors; break") break; last_happiness = effective_happiness # print("write trackers left: {}".format(len(write_trackers))) From b623a4a199d6d8c3e856866c9ba94e337eef7138 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:32:27 -0400 Subject: [PATCH 19/21] Remove dead Tor TCP control port setup code. If someone wants this I bet they can figure it out. --- src/allmydata/util/tor_provider.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/allmydata/util/tor_provider.py b/src/allmydata/util/tor_provider.py index ccb963775..14d871cd0 100644 --- a/src/allmydata/util/tor_provider.py +++ b/src/allmydata/util/tor_provider.py @@ -72,13 +72,9 @@ def _launch_tor(reactor, tor_executable, private_dir, txtorcon): tor_config = txtorcon.TorConfig() tor_config.DataDirectory = data_directory(private_dir) - if True: # unix-domain control socket - tor_config.ControlPort = "unix:" + os.path.join(private_dir, "tor.control") - tor_control_endpoint_desc = tor_config.ControlPort - else: - # we allocate a new TCP control port each time - tor_config.ControlPort = allocate_tcp_port() - tor_control_endpoint_desc = "tcp:127.0.0.1:%d" % tor_config.ControlPort + # unix-domain control socket + tor_config.ControlPort = "unix:" + os.path.join(private_dir, "tor.control") + tor_control_endpoint_desc = tor_config.ControlPort tor_config.SOCKSPort = allocate_tcp_port() From 60bebd0659fe195a62217d89cfa31b87d30bb214 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:35:56 -0400 Subject: [PATCH 20/21] Turn off another lgtm query. It's not good. --- .lgtm.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.lgtm.yml b/.lgtm.yml index 240bc62c5..07b5ff461 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -10,3 +10,8 @@ queries: # zope.interface choice to exclude self from method signatures. So, turn it # off. - exclude: "py/call/wrong-arguments" + + # The premise of this query is broken. The errors it produces are nonsense. + # There is no such thing as a "procedure" in Python and "None" is not + # meaningless. + - exclude: "py/procedure-return-value-used" From 97aff20cfde2445310dc9bac31691bbb5a11f2fa Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 26 Apr 2018 15:41:38 -0400 Subject: [PATCH 21/21] Disable another lgtm query. --- .lgtm.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.lgtm.yml b/.lgtm.yml index 07b5ff461..efc2479ca 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -15,3 +15,8 @@ queries: # There is no such thing as a "procedure" in Python and "None" is not # meaningless. - exclude: "py/procedure-return-value-used" + + # It is true that this query identifies things which are sometimes mistakes. + # However, it also identifies things which are entirely valid. Therefore, + # it produces noisy results. + - exclude: "py/implicit-string-concatenation-in-list" \ No newline at end of file