Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Updater module #308

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 5 additions & 4 deletions arelle/Cntlr.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class Cntlr:
True if a system platform clipboard is implemented on current platform

.. attribute:: updateURL
URL string of application download file (on arelle.org server). Usually redirected to latest released application installable module.
URL string of application download file (on arelle.org) or None if update installs not supported by platform.

"""
__version__ = "1.6.0"
Expand All @@ -109,6 +109,7 @@ def __init__(self, hasGui=False, logFileName=None, logFileMode=None, logFileEnco
self.isCGI = False
self.systemWordSize = int(round(math.log(sys.maxsize, 2)) + 1) # e.g., 32 or 64
self.uiLangDir = "ltr"
self.updateURL = None

# sys.setrecursionlimit(10000) # 1000 default exceeded in some inline documents

Expand Down Expand Up @@ -166,7 +167,7 @@ def __init__(self, hasGui=False, logFileName=None, logFileMode=None, logFileEnco
# note that cache is in ~/Library/Caches/Arelle
self.contextMenuClick = "<Button-2>"
self.hasClipboard = hasGui # clipboard always only if Gui (not command line mode)
self.updateURL = "http://arelle.org/download/1005"
self.updateURL = "https://arelle.org/download/1005"
elif sys.platform.startswith("win"):
self.isMac = False
self.isMSW = True
Expand All @@ -192,9 +193,9 @@ def __init__(self, hasGui=False, logFileName=None, logFileMode=None, logFileEnco
self.hasClipboard = False
self.contextMenuClick = "<Button-3>"
if "64 bit" in sys.version:
self.updateURL = "http://arelle.org/download/1008"
self.updateURL = "https://arelle.org/download/1008"
else: # 32 bit
self.updateURL = "http://arelle.org/download/1011"
self.updateURL = "https://arelle.org/download/1011"
else: # Unix/Linux
self.isMac = False
self.isMSW = False
Expand Down
4 changes: 3 additions & 1 deletion arelle/CntlrWinMain.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
@author: Mark V Systems Limited
(c) Copyright 2010 Mark V Systems Limited, All rights reserved.
'''
from __future__ import annotations

from arelle import PythonUtil # define 2.x or 3.x string types
import os, sys, subprocess, pickle, time, locale, re, fnmatch, platform

Expand Down Expand Up @@ -1424,7 +1426,7 @@ def uiReloadViews(self, modelXbrl):
view.view()

# worker threads showStatus
def showStatus(self, message, clearAfter=None):
def showStatus(self, message: str, clearAfter: int | None = None) -> None:
self.uiThreadQueue.put((self.uiShowStatus, [message, clearAfter]))

# ui thread showStatus
Expand Down
223 changes: 148 additions & 75 deletions arelle/Updater.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,159 @@
'''
"""
Created on May 30, 2010

@author: Mark V Systems Limited
(c) Copyright 2011 Mark V Systems Limited, All rights reserved.
'''
import tkinter.messagebox, webbrowser, os, threading

def checkForUpdates(cntlr):
if not cntlr.webCache.workOffline:
# check for updates in background
import threading
thread = threading.Thread(target=lambda c=cntlr: backgroundCheckForUpdates(c))
thread.daemon = True
thread.start()

def backgroundCheckForUpdates(cntlr):
actualUrl = None
"""
from __future__ import annotations

import gettext
import os
import subprocess
import sys
import threading
import tkinter.messagebox
import typing
from datetime import date

import regex

from arelle import Version

if typing.TYPE_CHECKING:
from arelle.CntlrWinMain import CntlrWinMain

_ = gettext.gettext

_MESSAGE_HEADER = "arelle\u2122 - Updater"
_ISO_DATE_PATTERN = regex.compile(
r"(?P<date>(?P<year>[0-9]{4})-(?P<month>0[1-9]|1[0-2])-(?P<day>0[1-9]|[12][0-9]|3[01]))"
)


def checkForUpdates(cntlr: CntlrWinMain) -> None:
thread = threading.Thread(daemon=True, target=lambda c=cntlr: _checkForUpdates(c))
thread.start()


def _checkForUpdates(cntlr: CntlrWinMain) -> None:
if cntlr.updateURL is None:
_showInfo(
cntlr,
_(
"""
Operating system not supported by update checker.
Please go to arelle.org to check for updates.
"""
),
)
return
if cntlr.webCache.workOffline:
_showInfo(cntlr, _("Disable offline mode to check for updates."))
return
cntlr.showStatus(_("Checking for updates to Arelle"))
try:
attachmentFileName = cntlr.webCache.getAttachmentFilename(cntlr.updateURL)
if attachmentFileName:
cntlr.showStatus("") # clear web loading status entry
cntlr.uiThreadQueue.put((checkUpdateUrl, [cntlr, attachmentFileName]))
except:
pass
cntlr.showStatus("") # clear web loading status entry

def checkUpdateUrl(cntlr, attachmentFileName):
# get latest header file
except RuntimeError as e:
_showWarning(
cntlr,
_("Failed to check for updates. URL {0}, {1}").format(cntlr.updateURL, e),
)
return
finally:
cntlr.showStatus("") # clear web loading status entry
cntlr.uiThreadQueue.put((_checkUpdateUrl, [cntlr, attachmentFileName]))


def _checkUpdateUrl(cntlr: CntlrWinMain, attachmentFileName: str) -> None:
filename = os.path.basename(attachmentFileName)
try:
from arelle import WebCache, Version
filename = os.path.basename(attachmentFileName)
if filename and "-20" in filename:
i = filename.index("-20") + 1
filenameDate = filename[i:i+10]
versionDate = Version.version[0:10]
if filenameDate > versionDate:
# newer
reply = tkinter.messagebox.askyesnocancel(
_("arelle\u2122 - Updater"),
_("Update {0} is available, running version is {1}. \n\nDownload now? \n\n(Arelle will exit before installing.)").format(
filenameDate, versionDate),
parent=cntlr.parent)
if reply is None:
return False
if reply:
thread = threading.Thread(target=lambda u=actualUrl: backgroundDownload(cntlr, u))
thread.daemon = True
thread.start()
else:
if filenameDate < versionDate:
msg = _("Arelle running version, {0}, is newer than the downloadable version, {1}.").format(
versionDate, filenameDate)
else:
msg = _("Arelle running version, {0}, is the same as the downloadable version.").format(
versionDate)
tkinter.messagebox.showwarning(_("arelle\u2122 - Updater"), msg, parent=cntlr.parent)

except:
pass

return

def backgroundDownload(cntlr, url):
filepathtmp = cntlr.webCache.getfilename(cntlr.updateURL, reload=True)
cntlr.modelManager.showStatus(_("Download ompleted"), 5000)
filepath = os.path.join(os.path.dirname(filepathtmp), os.path.basename(url))
os.rename(filepathtmp, filepath)
cntlr.uiThreadQueue.put((install, [cntlr,filepath]))

def install(cntlr,filepath):
import sys
if sys.platform.startswith("win"):
os.startfile(filepath)
currentVersion = _parseVersion(Version.version)
except ValueError:
_showWarning(cntlr, _("Unable to determine current version of Arelle."))
return
try:
updateVersion = _parseVersion(filename)
except ValueError:
_showWarning(cntlr, _("Unable to determine version of Arelle update."))
return
if updateVersion > currentVersion:
reply = tkinter.messagebox.askokcancel(
_(_MESSAGE_HEADER),
_(
"""
Update {0} is available, current version is {1}.

Download now?

(Arelle will exit before installing.)
"""
).format(updateVersion, currentVersion),
parent=cntlr.parent,
)
if reply:
_backgroundDownload(cntlr, attachmentFileName)
elif updateVersion < currentVersion:
_showInfo(
cntlr,
_("Current Arelle version {0} is newer than update {1}.").format(
currentVersion, updateVersion
),
)
else:
if sys.platform in ("darwin", "macos"):
command = 'open'
else: # linux/unix
command = 'xdg-open'
_showInfo(
cntlr,
_("Arelle is already running the latest version {0}.").format(
currentVersion
),
)


def _parseVersion(versionStr: str) -> date:
versionDateMatch = _ISO_DATE_PATTERN.search(versionStr)
if versionDateMatch is None:
raise ValueError(f"Unable to parse version date from {versionStr}")
return date.fromisoformat(versionDateMatch.group("date"))


def _backgroundDownload(cntlr: CntlrWinMain, attachmentFileName: str) -> None:
thread = threading.Thread(
daemon=True, target=lambda u=attachmentFileName: _download(cntlr, u)
)
thread.start()


def _download(cntlr: CntlrWinMain, url: str) -> None:
filepathTmp = cntlr.webCache.getfilename(cntlr.updateURL, reload=True)
if not filepathTmp:
_showWarning(cntlr, _("Failed to download update."))
return
cntlr.showStatus(_("Download completed"), 5000)
filepath = os.path.join(os.path.dirname(filepathTmp), os.path.basename(url))
try:
os.rename(filepathTmp, filepath)
except OSError:
_showWarning(cntlr, _("Failed to process update."))
return
cntlr.uiThreadQueue.put((_install, [cntlr, filepath]))


def _install(cntlr: CntlrWinMain, filepath: str) -> None:
if sys.platform == "win32":
os.startfile(filepath)
elif sys.platform == "darwin":
try:
import subprocess
subprocess.Popen([command,filepath])
except:
pass
subprocess.Popen(["open", filepath])
except (OSError, subprocess.SubprocessError):
_showWarning(cntlr, _("Failed to start updated Arelle instance."))
return
else:
raise RuntimeError("Tried to install update on unsupported platform.")
cntlr.uiThreadQueue.put((cntlr.quit, []))


def _showInfo(cntlr: CntlrWinMain, msg: str) -> None:
tkinter.messagebox.showinfo(_(_MESSAGE_HEADER), msg, parent=cntlr.parent)


def _showWarning(cntlr: CntlrWinMain, msg: str) -> None:
tkinter.messagebox.showwarning(_(_MESSAGE_HEADER), msg, parent=cntlr.parent)
21 changes: 12 additions & 9 deletions arelle/WebCache.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
e.g., User-Agent: Sample Company Name AdminContact@<sample company domain>.com

'''
from __future__ import annotations

import os, posixpath, sys, re, shutil, time, calendar, io, json, logging, shutil, cgi, zlib
if sys.version[0] >= '3':
from urllib.parse import quote, unquote
Expand Down Expand Up @@ -328,7 +330,10 @@ def cacheFilepathToUrl(self, cacheFilepath):
.sub(lambda c: chr( int(c.group(0)[1:]) ), # remove ^nnn encoding
urlpart) for urlpart in urlparts)

def getfilename(self, url, base=None, reload=False, checkModifiedTime=False, normalize=False, filenameOnly=False):
def getfilename(
self, url: str | None, base: str | None = None,
reload: bool = False, checkModifiedTime: bool = False,
normalize: bool = False, filenameOnly: bool = False) -> str | None:
if url is None:
return url
if base is not None or normalize:
Expand Down Expand Up @@ -641,14 +646,12 @@ def geturl(self, url): # get the url that the argument url redirects or resolve
pass
return None

def getAttachmentFilename(self, url): # get the filename attachment from the header
if url and isHttpUrl(url):
try:
fp = self.opener.open(url, timeout=self.timeout)
return cgi.parse_header(fp.headers.get("Content-Disposition"))[1]["filename"]
except Exception:
pass
return None
def getAttachmentFilename(self, url: str) -> str: # get the filename attachment from the header
try:
fp = self.opener.open(url, timeout=self.timeout)
return cgi.parse_header(fp.headers.get("Content-Disposition"))[1]["filename"]
except (URLError, IndexError, KeyError) as e:
raise RuntimeError(f"Unable to to retrieve filename from headers for {url}") from e

def retrieve(self, url, filename=None, filestream=None, reporthook=None, data=None):
# return filename, headers (in dict), initial file bytes (to detect logon requests)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ ignore_errors = true
[[tool.mypy.overrides]]
module = [
'arelle.plugin.validate.ESEF.*',
'arelle.Updater',
]
ignore_errors = false
strict = true