Use more f-strings

And a few more python3 features
diff --git a/emsdk.py b/emsdk.py
index 5f73452..19b9947 100644
--- a/emsdk.py
+++ b/emsdk.py
@@ -26,10 +26,9 @@
 
 from urllib.parse import urljoin
 from urllib.request import urlopen
-import functools
 
-if sys.version_info < (3, 0):
-  print(f'error: emsdk requires python 3.0 or above ({sys.executable} {sys.version})', file=sys.stderr)
+if sys.version_info < (3, 2):
+  print(f'error: emsdk requires python 3.2 or above ({sys.executable} {sys.version})', file=sys.stderr)
   sys.exit(1)
 
 emsdk_packages_url = 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/'
@@ -203,17 +202,17 @@
     return ('', '', None)
 
   if url.endswith(('/tree/', '/tree', '/commit/', '/commit')):
-    raise Exception('Malformed git URL and refspec ' + url + '!')
+    raise Exception(f'Malformed git URL and refspec {url}!')
 
   if '/tree/' in url:
     if url.endswith('/'):
-      raise Exception('Malformed git URL and refspec ' + url + '!')
+      raise Exception(f'Malformed git URL and refspec {url}!')
     url, refspec = url.split('/tree/')
     remote_name = url.split('/')[-2]
     return (url, refspec, remote_name)
   elif '/commit/' in url:
     if url.endswith('/'):
-      raise Exception('Malformed git URL and refspec ' + url + '!')
+      raise Exception(f'Malformed git URL and refspec {url}!')
     url, refspec = url.split('/commit/')
     remote_name = url.split('/')[-2]
     return (url, refspec, remote_name)
@@ -340,13 +339,13 @@
       # Register locally from CURRENT USER section.
       folder = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_ALL_ACCESS)
     winreg.SetValueEx(folder, key, 0, winreg.REG_EXPAND_SZ, value)
-    debug_print('Set key=' + key + ' with value ' + value + ' in registry.')
+    debug_print(f'Set key={key} with value {value} in registry.')
     return True
   except Exception as e:
     # 'Access is denied.'
     if e.args[3] == 5:
-      exit_with_error('failed to set the environment variable \'' + key + '\'! Setting environment variables permanently requires administrator access. Please rerun this command with administrative privileges. This can be done for example by holding down the Ctrl and Shift keys while opening a command prompt in start menu.')
-    errlog('Failed to write environment variable ' + key + ':')
+      exit_with_error(f'failed to set the environment variable \'{key}\'! Setting environment variables permanently requires administrator access. Please rerun this command with administrative privileges. This can be done for example by holding down the Ctrl and Shift keys while opening a command prompt in start menu.')
+    errlog(f'Failed to write environment variable {key}:')
     errlog(str(e))
     return False
   finally:
@@ -384,7 +383,7 @@
     # this catch is if both the registry key threw an exception and the key is not in os.environ
     if e.args[0] != 2:
       # 'The system cannot find the file specified.'
-      errlog('Failed to read environment variable ' + key + ':')
+      errlog(f'Failed to read environment variable {key}:')
       errlog(str(e))
     return None
   return value
@@ -416,16 +415,16 @@
     # Escape % signs so that we don't expand references to environment variables.
     value = value.replace('%', '^%')
     if len(value) >= 1024:
-      exit_with_error('the new environment variable ' + key + ' is more than 1024 characters long! A value this long cannot be set via command line: please add the environment variable specified above to system environment manually via Control Panel.')
+      exit_with_error(f'the new environment variable {key} is more than 1024 characters long! A value this long cannot be set via command line: please add the environment variable specified above to system environment manually via Control Panel.')
     cmd = ['SETX', key, value]
     debug_print(str(cmd))
     retcode = subprocess.call(cmd, stdout=subprocess.PIPE)
     if retcode != 0:
-      errlog('ERROR! Failed to set environment variable ' + key + '=' + value + '. You may need to set it manually.')
+      errlog(f'ERROR! Failed to set environment variable {key}={value}. You may need to set it manually.')
     else:
       return True
   except Exception as e:
-    errlog('ERROR! Failed to set environment variable ' + key + '=' + value + ':')
+    errlog(f'ERROR! Failed to set environment variable {key}={value}:')
     errlog(str(e))
     errlog('You may need to set it manually.')
 
@@ -467,7 +466,7 @@
 
 
 def win_delete_environment_variable(key, system=True, user=True):
-  debug_print('win_delete_environment_variable(key=' + key + ', system=' + str(system) + ')')
+  debug_print(f'win_delete_environment_variable(key={key}, system={system})')
   return win_set_environment_variable(key, None, system, user)
 
 
@@ -481,19 +480,14 @@
 
 # Removes a single file, suppressing exceptions on failure.
 def rmfile(filename):
-  debug_print('rmfile(' + filename + ')')
+  debug_print(f'rmfile({filename})')
   if os.path.lexists(filename):
     os.remove(filename)
 
 
-# http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python
 def mkdir_p(path):
-  debug_print('mkdir_p(' + path + ')')
-  try:
-    os.makedirs(path)
-  except OSError as exc:  # Python >2.5
-    if exc.errno != errno.EEXIST or not os.path.isdir(path):
-      raise
+  debug_print(f'mkdir_p({path})')
+  os.makedirs(path, exist_ok=True)
 
 
 def is_nonempty_directory(path):
@@ -513,7 +507,7 @@
 
 # http://pythonicprose.blogspot.fi/2009/10/python-extract-targz-archive.html
 def untargz(source_filename, dest_dir):
-  print("Unpacking '" + source_filename + "' to '" + dest_dir + "'")
+  print(f"Unpacking '{source_filename}' to '{dest_dir}'")
   mkdir_p(dest_dir)
   returncode = run(['tar', '-xvf' if VERBOSE else '-xf', sdk_path(source_filename), '--strip', '1'], cwd=dest_dir)
   # tfile = tarfile.open(source_filename, 'r:gz')
@@ -531,7 +525,7 @@
   # Test if emsdk calls fix_potentially_long_windows_pathname() with long
   # relative paths (which is problematic)
   if not os.path.isabs(pathname) and len(pathname) > 200:
-    errlog('Warning: Seeing a relative path "' + pathname + '" which is dangerously long for being referenced as a short Windows path name. Refactor emsdk to be able to handle this!')
+    errlog(f'Warning: Seeing a relative path "{pathname}" which is dangerously long for being referenced as a short Windows path name. Refactor emsdk to be able to handle this!')
   if pathname.startswith('\\\\?\\'):
     return pathname
   pathname = os.path.normpath(pathname.replace('/', '\\'))
@@ -554,7 +548,7 @@
 
 # http://stackoverflow.com/questions/12886768/simple-way-to-unzip-file-in-python-on-all-oses
 def unzip(source_filename, dest_dir):
-  print("Unpacking '" + source_filename + "' to '" + dest_dir + "'")
+  print(f"Unpacking '{source_filename}' to '{dest_dir}'")
   mkdir_p(dest_dir)
   common_subdir = None
   try:
@@ -609,11 +603,11 @@
       if common_subdir:
         remove_tree(unzip_to_dir)
   except zipfile.BadZipfile as e:
-    errlog("Unzipping file '" + source_filename + "' failed due to reason: " + str(e) + "! Removing the corrupted zip file.")
+    errlog(f"Unzipping file '{source_filename}' failed due to reason: {e}! Removing the corrupted zip file.")
     rmfile(source_filename)
     return False
   except Exception as e:
-    errlog("Unzipping file '" + source_filename + "' failed due to reason: " + str(e))
+    errlog(f"Unzipping file '{source_filename}' failed due to reason: {e}")
     return False
 
   return True
@@ -725,11 +719,11 @@
 # On failure, returns None.
 def download_file(url, dstpath, download_even_if_exists=False,
                   filename_prefix='', silent=False):
-  debug_print('download_file(url=' + url + ', dstpath=' + dstpath + ')')
+  debug_print(f'download_file(url={url}, dstpath={dstpath})')
   file_name = get_download_target(url, dstpath, filename_prefix)
 
   if os.path.exists(file_name) and not download_even_if_exists:
-    print("File '" + file_name + "' already downloaded, skipping.")
+    print(f"File '{file_name}' already downloaded, skipping.")
     return file_name
 
   mkdir_p(os.path.dirname(file_name))
@@ -744,7 +738,7 @@
     else:
       download_with_urllib(url, file_name)
   except Exception as e:
-    errlog("Error: Downloading URL '" + url + "': " + str(e))
+    errlog(f"Error: Downloading URL '{url}': {e}")
     return None
   except KeyboardInterrupt:
     rmfile(file_name)
@@ -754,7 +748,7 @@
 
 
 def run_get_output(cmd, cwd=None):
-  debug_print('run_get_output(cmd=' + str(cmd) + ', cwd=' + str(cwd) + ')')
+  debug_print(f'run_get_output(cmd={cmd}, cwd={cwd})')
   process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=os.environ.copy(), universal_newlines=True)
   stdout, stderr = process.communicate()
   return (process.returncode, stdout, stderr)
@@ -822,31 +816,31 @@
 
 
 def git_clone(url, dstpath, branch, remote_name='origin'):
-  debug_print('git_clone(url=' + url + ', dstpath=' + dstpath + ')')
+  debug_print(f'git_clone(url={url}, dstpath={dstpath})')
   if os.path.isdir(os.path.join(dstpath, '.git')):
     remotes = get_git_remotes(dstpath)
     if remote_name in remotes:
-      debug_print('Repository ' + url + ' with remote "' + remote_name + '" already cloned to directory ' + dstpath + ', skipping.')
+      debug_print(f'Repository {url} with remote "{remote_name}" already cloned to directory {dstpath}, skipping.')
       return True
     else:
-      debug_print('Repository ' + url + ' with remote "' + remote_name + '" already cloned to directory ' + dstpath + ', but remote has not yet been added. Creating.')
+      debug_print(f'Repository {url} with remote "{remote_name}" already cloned to directory {dstpath}, but remote has not yet been added. Creating.')
       return run([GIT(), 'remote', 'add', remote_name, url], cwd=dstpath) == 0
 
   mkdir_p(dstpath)
   git_clone_args = ['--recurse-submodules', '--branch', branch]  # Do not check out a branch (installer will issue a checkout command right after)
   if GIT_CLONE_SHALLOW:
     git_clone_args += ['--depth', '1']
-  print('Cloning from ' + url + '...')
+  print(f'Cloning from {url}...')
   return run([GIT(), 'clone', '-o', remote_name] + git_clone_args + [url, dstpath]) == 0
 
 
 def git_pull(repo_path, branch_or_tag, remote_name='origin'):
-  debug_print('git_pull(repo_path=' + repo_path + ', branch/tag=' + branch_or_tag + ', remote_name=' + remote_name + ')')
+  debug_print(f'git_pull(repo_path={repo_path}, branch/tag={branch_or_tag}, remote_name={remote_name})')
   ret = run([GIT(), 'fetch', '--quiet', remote_name], repo_path)
   if ret != 0:
     return False
   try:
-    print("Fetching latest changes to the branch/tag '" + branch_or_tag + "' for '" + repo_path + "'...")
+    print(f"Fetching latest changes to the branch/tag '{branch_or_tag}' for '{repo_path}'...")
     ret = run([GIT(), 'fetch', '--quiet', remote_name], repo_path)
     if ret != 0:
       return False
@@ -871,13 +865,13 @@
   except Exception:
     errlog('git operation failed!')
     return False
-  print("Successfully updated and checked out branch/tag '" + branch_or_tag + "' on repository '" + repo_path + "'")
+  print(f"Successfully updated and checked out branch/tag '{branch_or_tag}' on repository '{repo_path}'")
   print("Current repository version: " + git_repo_version(repo_path))
   return True
 
 
 def git_clone_checkout_and_pull(url, dstpath, branch, override_remote_name='origin'):
-  debug_print('git_clone_checkout_and_pull(url=' + url + ', dstpath=' + dstpath + ', branch=' + branch + ', override_remote_name=' + override_remote_name + ')')
+  debug_print(f'git_clone_checkout_and_pull(url={url}, dstpath={dstpath}, branch={branch}, override_remote_name={override_remote_name})')
 
   # Make sure the repository is cloned first
   success = git_clone(url, dstpath, branch, override_remote_name)
@@ -970,13 +964,13 @@
     if tool.id == 'cmake' and tool.is_active():
       cmake_exe = locate_cmake_from_tool(tool)
       if cmake_exe:
-        info('Found installed+activated CMake tool at "' + cmake_exe + '"')
+        info(f'Found installed+activated CMake tool at "{cmake_exe}"')
         return cmake_exe
 
   # 2. If cmake already exists in PATH, then use that cmake to configure the build.
   cmake_exe = which('cmake')
   if cmake_exe:
-    info('Found CMake from PATH at "' + cmake_exe + '"')
+    info(f'Found CMake from PATH at "{cmake_exe}"')
     return cmake_exe
 
   # 3. Finally, if user has installed a cmake tool, but has not activated that, then use
@@ -987,14 +981,14 @@
     if tool.id == 'cmake' and tool.is_installed():
       cmake_exe = locate_cmake_from_tool(tool)
       if cmake_exe:
-        info('Found installed CMake tool at "' + cmake_exe + '"')
+        info(f'Found installed CMake tool at "{cmake_exe}"')
         return cmake_exe
 
   exit_with_error('Unable to find "cmake" in PATH, or as installed/activated tool! Please install CMake first')
 
 
 def make_build(build_root, build_type):
-  debug_print('make_build(build_root=' + build_root + ', build_type=' + build_type + ')')
+  debug_print(f'make_build(build_root={build_root}, build_type={build_type})')
   if CPU_CORES > 1:
     print('Performing a parallel build with ' + str(CPU_CORES) + ' cores.')
   else:
@@ -1018,7 +1012,7 @@
     print('Running build: ' + str(make))
     ret = subprocess.check_call(make, cwd=build_root, env=build_env(CMAKE_GENERATOR))
     if ret != 0:
-      errlog('Build failed with exit code ' + ret + '!')
+      errlog('Build failed with exit code {ret}!')
       errlog('Working directory: ' + build_root)
       return False
   except Exception as e:
@@ -1065,7 +1059,7 @@
     open(os.path.join(build_root, 'recmake.' + ('bat' if WINDOWS else 'sh')), 'w').write(' '.join(map(quote_parens, cmdline)))
     ret = subprocess.check_call(cmdline, cwd=build_root, env=build_env(CMAKE_GENERATOR))
     if ret != 0:
-      errlog('CMake invocation failed with exit code ' + ret + '!')
+      errlog('CMake invocation failed with exit code {ret}!')
       errlog('Working directory: ' + build_root)
       return False
   except OSError as e:
@@ -1387,13 +1381,13 @@
 
     # Abort if detaching was not successful
     if os.path.exists(mount_point):
-      raise Exception('Previous mount of Firefox already exists at "' + mount_point + '", unable to proceed.')
+      raise Exception(f'Previous mount of Firefox already exists at "{mount_point}", unable to proceed.')
 
     # Mount the archive
     run(['hdiutil', 'attach', filename])
     firefox_dir = os.path.join(mount_point, app_name)
     if not os.path.isdir(firefox_dir):
-      raise Exception('Unable to find Firefox directory "' + firefox_dir + '" inside app image.')
+      raise Exception(f'Unable to find Firefox directory "{firefox_dir}" inside app image.')
 
     # And install by copying the files from the archive
     shutil.copytree(firefox_dir, root)
@@ -1522,7 +1516,7 @@
 def uninstall_binaryen(tool):
   debug_print('uninstall_binaryen(' + str(tool) + ')')
   build_root = binaryen_build_root(tool)
-  print("Deleting path '" + build_root + "'")
+  print(f"Deleting path '{build_root}'")
   remove_tree(build_root)
 
 
@@ -1563,7 +1557,7 @@
 
 
 def download_and_extract(archive, dest_dir, filename_prefix='', clobber=True):
-  debug_print('download_and_extract(archive=' + archive + ', dest_dir=' + dest_dir + ')')
+  debug_print('download_and_extract(archive={archive}, dest_dir={dest_dir})')
 
   url = urljoin(emsdk_packages_url, archive)
 
@@ -1723,7 +1717,7 @@
 def download_node_nightly(tool):
   nightly_versions = fetch_nightly_node_versions()
   latest_nightly = extract_newest_node_nightly_version(nightly_versions)
-  print('Latest Node.js Nightly download available is "' + latest_nightly + '"')
+  print('Latest Node.js Nightly download available is "{latest_nightly}"')
 
   output_dir = os.path.abspath('node/nightly-' + latest_nightly)
   # Node.js zip structure quirk: Linux and macOS archives have a /bin,
@@ -1794,9 +1788,9 @@
 
   for name, value in activated_config.items():
     if value.startswith('['):
-      cfg += name + " = " + value + "\n"
+      cfg += f'{name} = {value}\n'
     else:
-      cfg += name + " = '" + value + "'\n"
+      cfg += f"{name} = '{value}'\n"
 
   emroot = find_emscripten_root(active_tools)
   if emroot:
@@ -1856,14 +1850,11 @@
   return ''
 
 
-class Tool(object):
+class Tool:
   def __init__(self, data):
     # Convert the dictionary representation of the tool in 'data' to members of
     # this class for convenience.
     for key, value in data.items():
-      # Python2 compat, convert unicode to str
-      if sys.version_info < (3,) and isinstance(value, unicode): # noqa
-        value = value.encode('Latin-1')
       setattr(self, key, value)
 
     # Cache the name ID of this Tool (these are read very often)
@@ -1916,7 +1907,7 @@
       for tool_name in self.uses:
         tool = find_tool(tool_name)
         if not tool:
-          debug_print('Tool ' + str(self) + ' depends on ' + tool_name + ' which does not exist!')
+          debug_print(f'Tool {self} depends on {tool_name} which does not exist!')
           continue
         if tool.needs_compilation():
           return True
@@ -2024,7 +2015,7 @@
       for tool_name in self.uses:
         tool = find_tool(tool_name)
         if tool is None:
-          errlog("Manifest error: No tool by name '" + tool_name + "' found! This may indicate an internal SDK error!")
+          errlog(f"Manifest error: No tool by name '{tool_name}' found! This may indicate an internal SDK error!")
           return False
         if not tool.is_installed():
           return False
@@ -2072,7 +2063,7 @@
 
     for key, value in activated_cfg.items():
       if key not in EM_CONFIG_DICT:
-        debug_print(str(self) + ' is not active, because key="' + key + '" does not exist in .emscripten')
+        debug_print(f'{self} is not active, because key="{key}" does not exist in .emscripten')
         return False
 
       # all paths are stored dynamically relative to the emsdk root, so
@@ -2080,7 +2071,7 @@
       config_value = EM_CONFIG_DICT[key].replace("emsdk_path + '", "'" + EMSDK_PATH)
       config_value = config_value.strip("'")
       if config_value != value:
-        debug_print(str(self) + ' is not active, because key="' + key + '" has value "' + config_value + '" but should have value "' + value + '"')
+        debug_print(f'{self} is not active, because key="{key}" has value "{config_value}" but should have value "{value}"')
         return False
     return True
 
@@ -2090,7 +2081,7 @@
     for env in envs:
       key, value = parse_key_value(env)
       if key not in os.environ or to_unix_path(os.environ[key]) != to_unix_path(value):
-        debug_print(str(self) + ' is not active, because environment variable key="' + key + '" has value "' + str(os.getenv(key)) + '" but should have value "' + value + '"')
+        debug_print(f'{self} is not active, because environment variable key="{key}" has value "{os.getenv(key)}" but should have value "{value}"')
         return False
 
     if hasattr(self, 'activated_path'):
@@ -2098,7 +2089,7 @@
       for p in path:
         path_items = os.environ['PATH'].replace('\\', '/').split(ENVPATH_SEPARATOR)
         if not normalized_contains(path_items, p):
-          debug_print(str(self) + ' is not active, because environment variable PATH item "' + p + '" is not present (PATH=' + os.environ['PATH'] + ')')
+          debug_print(f'{self} is not active, because environment variable PATH item "{p}" is not present (PATH={os.environ["PATH"]})')
           return False
     return True
 
@@ -2130,7 +2121,7 @@
     already being installed.
     """
     if self.can_be_installed() is not True:
-      exit_with_error("The tool '" + str(self) + "' is not available due to the reason: " + self.can_be_installed())
+      exit_with_error(f"The tool '{self}' is not available due to the reason: {self.can_be_installed()}")
 
     if self.id == 'sdk':
       return self.install_sdk()
@@ -2141,17 +2132,17 @@
     """Returns True if any SDK component was installed of False all componented
     were already installed.
     """
-    print("Installing SDK '" + str(self) + "'..")
+    print(f"Installing SDK '{self}'..")
     installed = False
 
     for tool_name in self.uses:
       tool = find_tool(tool_name)
       if tool is None:
-        exit_with_error("manifest error: No tool by name '" + tool_name + "' found! This may indicate an internal SDK error!")
+        exit_with_error(f"manifest error: No tool by name '{tool_name}' found! This may indicate an internal SDK error!")
       installed |= tool.install()
 
     if not installed:
-      print("All SDK components already installed: '" + str(self) + "'.")
+      print(f"All SDK components already installed: '{self}'.")
       return False
 
     if getattr(self, 'custom_install_script', None) == 'emscripten_npm_install':
@@ -2164,7 +2155,7 @@
         if not emscripten_npm_install(self, emscripten_dir):
           exit_with_error('post-install step failed: emscripten_npm_install')
 
-    print("Done installing SDK '" + str(self) + "'.")
+    print(f"Done installing SDK '{self}'.")
     return True
 
   def install_tool(self):
@@ -2176,10 +2167,10 @@
     # installed every time when requested, since the install step is then used to git
     # pull the tool to a newer version.
     if self.is_installed() and not hasattr(self, 'git_branch'):
-      print("Skipped installing " + self.name + ", already installed.")
+      print(f"Skipped installing {self.name}, already installed.")
       return False
 
-    print("Installing tool '" + str(self) + "'..")
+    print(f"Installing tool '{self}'..")
     url = self.download_url()
 
     custom_install_scripts = {
@@ -2227,12 +2218,12 @@
         with open(emscripten_version_file_path, 'w') as f:
           f.write('"%s"\n' % version)
 
-    print("Done installing tool '" + str(self) + "'.")
+    print(f"Done installing tool '{self}'.")
 
     # Sanity check that the installation succeeded, and if so, remove unneeded
     # leftover installation files.
     if not self.is_installed(skip_version_check=True):
-      exit_with_error("installation of '" + str(self) + "' failed, but no error was detected. Either something went wrong with the installation, or this may indicate an internal emsdk error.")
+      exit_with_error(f"installation of '{self}' failed, but no error was detected. Either something went wrong with the installation, or this may indicate an internal emsdk error.")
 
     self.cleanup_temp_install_files()
     self.update_installed_version()
@@ -2244,22 +2235,22 @@
     url = self.download_url()
     if url.endswith(ARCHIVE_SUFFIXES):
       download_target = get_download_target(url, download_dir, getattr(self, 'download_prefix', ''))
-      debug_print("Deleting temporary download: " + download_target)
+      debug_print(f"Deleting temporary download: {download_target}")
       rmfile(download_target)
 
   def uninstall(self):
     if not self.is_installed():
-      print("Tool '" + str(self) + "' was not installed. No need to uninstall.")
+      print(f"Tool '{self}' was not installed. No need to uninstall.")
       return
-    print("Uninstalling tool '" + str(self) + "'..")
+    print(f"Uninstalling tool '{self}'..")
     if hasattr(self, 'custom_uninstall_script'):
       if self.custom_uninstall_script == 'uninstall_binaryen':
         uninstall_binaryen(self)
       else:
-        raise Exception('Unknown custom_uninstall_script directive "' + self.custom_uninstall_script + '"!')
-    print("Deleting path '" + self.installation_path() + "'")
+        raise Exception(f'Unknown custom_uninstall_script directive "{self.custom_uninstall_script}"!')
+    print(f"Deleting path '{self.installation_path()}'")
     remove_tree(self.installation_path())
-    print("Done uninstalling '" + str(self) + "'.")
+    print(f"Done uninstalling '{self}'.")
 
   def dependencies(self):
     if not hasattr(self, 'uses'):
@@ -2412,22 +2403,13 @@
 
 
 def get_release_hash(arg, releases_info):
-  return releases_info.get(arg, None) or releases_info.get('sdk-' + arg + '-64bit')
+  return releases_info.get(arg, None) or releases_info.get(f'sdk-{arg}-64bit')
 
 
 def version_key(ver):
   return tuple(map(int, re.split('[._-]', ver)[:3]))
 
 
-# A sort function that is compatible with both Python 2 and Python 3 using a
-# custom comparison function.
-def python_2_3_sorted(arr, cmp):
-  if sys.version_info >= (3,):
-    return sorted(arr, key=functools.cmp_to_key(cmp))
-  else:
-    return sorted(arr, cmp=cmp)
-
-
 def is_emsdk_sourced_from_github():
   return os.path.exists(os.path.join(EMSDK_PATH, '.git'))
 
@@ -2524,12 +2506,6 @@
   return versions
 
 
-def is_string(s):
-  if sys.version_info[0] >= 3:
-    return isinstance(s, str)
-  return isinstance(s, basestring)  # noqa
-
-
 def load_sdk_manifest():
   try:
     manifest = json.loads(open(sdk_path("emsdk_manifest.json"), "r").read())
@@ -2566,7 +2542,7 @@
       return version_key(ver) == version_key(reference)
     if cmp_operand == '!=':
       return version_key(ver) != version_key(reference)
-    raise Exception('Invalid cmp_operand "' + cmp_operand + '"!')
+    raise Exception(f'Invalid cmp_operand "{cmp_operand}"!')
 
   def passes_filters(param, ver, filters):
     for v in filters:
@@ -2583,7 +2559,7 @@
       t2 = copy.copy(t)
       found_param = False
       for p, v in vars(t2).items():
-        if is_string(v) and param in v:
+        if isinstance(v, str) and param in v:
           t2.__dict__[p] = v.replace(param, ver)
           found_param = True
       if not found_param:
@@ -2889,15 +2865,15 @@
         continue
       info(key + ' = ' + value)
       if POWERSHELL:
-        env_string += '$env:' + key + '="' + value + '"\n'
+        env_string += f'$env:{key}="{value}"\n'
       elif CMD:
-        env_string += 'SET ' + key + '=' + value + '\n'
+        env_string += f'SET {key}={value}\n'
       elif CSH:
-        env_string += 'setenv ' + key + ' "' + value + '";\n'
+        env_string += f'setenv {key} "{value}";\n'
       elif FISH:
-        env_string += 'set -gx ' + key + ' "' + value + '";\n'
+        env_string += f'set -gx {key} "{value}";\n'
       elif BASH:
-        env_string += 'export ' + key + '="' + value + '";\n'
+        env_string += f'export {key}="{value}";\n'
       else:
         assert False
 
@@ -3162,11 +3138,11 @@
     tool_name, url_and_refspec = forked_url.split('@')
     t = find_tool(tool_name)
     if not t:
-      errlog('Failed to find tool ' + tool_name + '!')
+      errlog('Failed to find tool {tool_name}!')
       return False
     else:
       t.url, t.git_branch, t.remote_name = parse_github_url_and_refspec(url_and_refspec)
-      debug_print('Reading git repository URL "' + t.url + '" and git branch "' + t.git_branch + '" for Tool "' + tool_name + '".')
+      debug_print(f'Reading git repository URL "{t.url}" and git branch "{t.git_branch}" for Tool "{tool_name}".')
 
     forked_url = extract_string_arg('--override-repository')
 
@@ -3179,7 +3155,7 @@
         CMAKE_GENERATOR = build_generator.group(1)
         args[i] = ''
       else:
-        errlog("Cannot parse CMake generator string: " + args[i] + ". Try wrapping generator string with quotes")
+        errlog(f"Cannot parse CMake generator string: {args[i]}. Try wrapping generator string with quotes")
         return 1
     elif args[i].startswith('--build='):
       build_type = re.match(r'^--build=(.+)$', args[i])
@@ -3192,10 +3168,10 @@
           CMAKE_BUILD_TYPE_OVERRIDE = build_types[build_type_index]
           args[i] = ''
         except Exception:
-          errlog('Unknown CMake build type "' + build_type + '" specified! Please specify one of ' + str(build_types))
+          errlog(f'Unknown CMake build type "{build_type}" specified! Please specify one of {build_types}')
           return 1
       else:
-        errlog("Invalid command line parameter " + args[i] + ' specified!')
+        errlog(f'Invalid command line parameter {args[i]} specified!')
         return 1
   args = [x for x in args if x]
 
@@ -3301,7 +3277,7 @@
     print('Items marked with * are activated for the current user.')
     if has_partially_active_tools[0]:
       env_cmd = 'emsdk_env.bat' if WINDOWS else 'source ./emsdk_env.sh'
-      print('Items marked with (*) are selected for use, but your current shell environment is not configured to use them. Type "' + env_cmd + '" to set up your current shell to use them' + (', or call "emsdk activate --permanent <name_of_sdk>" to permanently activate them.' if WINDOWS else '.'))
+      print(f'Items marked with (*) are selected for use, but your current shell environment is not configured to use them. Type "{env_cmd}" to set up your current shell to use them' + (', or call "emsdk activate --permanent <name_of_sdk>" to permanently activate them.' if WINDOWS else '.'))
     if not arg_old:
       print('')
       print("To access the historical archived versions, type 'emsdk list --old'")
@@ -3353,7 +3329,7 @@
         print('Deactivating tool ' + str(tool) + '.')
         tools_to_activate.remove(tool)
       else:
-        print('Tool "' + arg + '" was not active, no need to deactivate.')
+        print(f'Tool "{arg}" was not active, no need to deactivate.')
     if not tools_to_activate:
       errlog('No tools/SDKs specified to activate! Usage:\n   emsdk activate tool/sdk1 [tool/sdk2] [...]')
       return 1
@@ -3375,7 +3351,7 @@
           CPU_CORES = int(multicore.group(1))
           args[i] = ''
         else:
-          errlog("Invalid command line parameter " + args[i] + ' specified!')
+          errlog(f'Invalid command line parameter {args[i]} specified!')
           return 1
       elif args[i] == '--shallow':
         GIT_CLONE_SHALLOW = True
@@ -3412,12 +3388,12 @@
       return 1
     tool = find_tool(args[0])
     if tool is None:
-      errlog("Error: Tool by name '" + args[0] + "' was not found.")
+      errlog(f"Error: Tool by name '{args[0]}' was not found.")
       return 1
     tool.uninstall()
     return 0
 
-  errlog("Unknown command '" + cmd + "' given! Type 'emsdk help' to get a list of commands.")
+  errlog(f"Unknown command '{cmd}' given! Type 'emsdk help' to get a list of commands.")
   return 1
 
 
diff --git a/scripts/update_python.py b/scripts/update_python.py
index 98273f1..f80327d 100755
--- a/scripts/update_python.py
+++ b/scripts/update_python.py
@@ -28,7 +28,6 @@
 import platform
 import urllib.request
 import shutil
-import subprocess
 import sys
 from subprocess import check_call
 from zip import unzip_cmd, zip_cmd