diff --git a/.gitignore b/.gitignore
index 814731b..d1c8c2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,18 @@ installer
feeds.dat
log.txt
settings.dat
+
+__pycache__
+.vscode
+.mypy_cache
+.venv
+
+parser.out
+parsetab.py
+
+*.pyc
+*.bak
+*.log
+
+Pipfile.lock
+
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..7f3e065
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,31 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+pylint = "*"
+autopep8 = "*"
+jupyter = "*"
+notebook = "*"
+ipython = "*"
+flake8 = "*"
+flake8-mypy = "*"
+mypy-lang = "*"
+isort = "*"
+winpdb-reborn = "==2.0.0.dev5"
+ipdb = "*"
+pycallgraph = "*"
+psutil = "*"
+pudb = "*"
+
+[packages]
+pygame = "*"
+wxpython = "*"
+feedparser = "*"
+ply = "*"
+pywin32 = {version = "*",sys_platform = "== 'win32'"}
+py2exe = {version = "*",sys_platform = "== 'win32'"}
+# winsound = {version = "*",sys_platform = "== 'win32'"}
+#[requires]
+#python_version = "3.8"
diff --git a/controller.py b/controller.py
index 54f3535..8368ca3 100644
--- a/controller.py
+++ b/controller.py
@@ -1,144 +1,272 @@
-import wx
-import idle
-import feeds
-import popups
-import view
-import updater
-import util
-import winsound
-import socket
-from settings import settings
-
-class Controller(object):
- def __init__(self):
- socket.setdefaulttimeout(settings.SOCKET_TIMEOUT)
- self.icon = view.TaskBarIcon(self)
- self.manager = feeds.FeedManager()
- self.manager.load()
- self.add_default_feeds()
- self.popup = None
- self.polling = False
- self.enabled = True
- self.on_poll()
- self.on_check_for_updates()
- def add_default_feeds(self):
- if self.manager.feeds:
- return
- for url in settings.DEFAULT_FEED_URLS:
- feed = feeds.Feed(url)
- feed.interval = 60 * 60 * 24
- self.manager.add_feed(feed)
- def parse_args(self, message):
- urls = message.split('\n')
- for url in urls:
- url = url.strip()
- if not url:
- continue
- self.add_feed(url)
- def enable(self):
- self.icon.set_icon('icons/feed.png')
- self.enabled = True
- self.poll()
- def disable(self):
- self.icon.set_icon('icons/feed_disabled.png')
- self.enabled = False
- def save(self):
- self.manager.save()
- def on_check_for_updates(self):
- try:
- self.check_for_updates(False)
- finally:
- wx.CallLater(1000 * 60 * 5, self.on_check_for_updates)
- def check_for_updates(self, force=True):
- updater.run(self, force)
- def on_poll(self):
- try:
- self.poll()
- finally:
- wx.CallLater(1000 * 5, self.on_poll)
- def poll(self):
- if self.polling:
- return
- if not self.enabled:
- return
- if settings.DISABLE_WHEN_IDLE and idle.get_idle_duration() > settings.USER_IDLE_TIMEOUT:
- return
- if not self.manager.should_poll():
- return
- self.polling = True
- self.icon.set_icon('icons/feed_go.png')
- util.start_thread(self._poll_thread)
- def _poll_thread(self):
- found_new = False
- try:
- for new_items in self.manager.poll():
- found_new = True
- wx.CallAfter(self._poll_result, new_items)
- finally:
- wx.CallAfter(self._poll_complete, found_new)
- def _poll_result(self, new_items):
- items = self.manager.items
- if self.popup:
- index = self.popup.index
- else:
- index = len(items)
- items.extend(new_items)
- self.show_items(items, index, False)
- def _poll_complete(self, found_new):
- if found_new:
- self.save()
- self.polling = False
- self.icon.set_icon('icons/feed.png')
- def force_poll(self):
- for feed in self.manager.feeds:
- feed.last_poll = 0
- self.poll()
- def show_items(self, items, index, focus):
- play_sound = False
- if not items:
- return
- if not self.popup:
- self.popup = popups.PopupManager()
- self.popup.Bind(popups.EVT_POPUP_CLOSE, self.on_popup_close)
- if not focus:
- play_sound = True
- self.popup.set_items(items, index, focus)
- if focus:
- self.popup.auto = False
- if play_sound:
- self.play_sound()
- def play_sound(self):
- if settings.PLAY_SOUND:
- path = settings.SOUND_PATH
- flags = winsound.SND_FILENAME | winsound.SND_ASYNC
- try:
- winsound.PlaySound(path, flags)
- except Exception:
- pass
- def show_popup(self):
- items = self.manager.items
- index = len(items) - 1
- self.show_items(items, index, True)
- def add_feed(self, url=''):
- feed = view.AddFeedDialog.show_wizard(None, url)
- if not feed:
- return
- self.manager.add_feed(feed)
- self.save()
- self.poll()
- def edit_settings(self):
- window = view.SettingsDialog(None, self)
- window.Center()
- window.ShowModal()
- window.Destroy()
- def close(self):
- try:
- if self.popup:
- self.popup.on_close()
- wx.CallAfter(self.icon.Destroy)
- finally:
- pass #wx.GetApp().ExitMainLoop()
- def on_popup_close(self, event):
- self.popup = None
- self.manager.purge_items(settings.ITEM_CACHE_AGE)
-
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+"""[summary]
+
+Returns:
+ [type] -- [description]
+"""
+
+import logging
+import socket
+import sys
+
+try:
+ import wx
+except ModuleNotFoundError:
+ sys.exit('\n\timport wx\n')
+
+import feeds
+import idle
+import popups
+import updater
+import util
+import view
+from settings import settings
+
+if sys.platform.startswith('win32'):
+ import winsound
+
+
+class Controller(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self):
+ """[summary]
+ """
+
+ print('Initializing Controller class') # FIXME: Delete this
+
+ socket.setdefaulttimeout(settings.SOCKET_TIMEOUT)
+ self.icon = view.TaskBarIcon(self)
+ self.manager = feeds.FeedManager()
+ self.manager.load()
+ self.add_default_feeds()
+ self.popup = None
+ self.polling = False
+ self.enabled = True
+ self.on_poll()
+ self.on_check_for_updates()
+
+ print('Initialized Controller class') # FIXME: Delete this
+
+ def add_default_feeds(self):
+ """[summary]
+ """
+
+ if self.manager.feeds:
+ return
+ for url in settings.DEFAULT_FEED_URLS:
+ feed = feeds.Feed(url)
+ feed.interval = 60 * 60 * 24
+ self.manager.add_feed(feed)
+
+ def parse_args(self, message):
+ """[summary]
+
+ Arguments:
+ message {[type]} -- [description]
+ """
+
+ urls = message.split('\n')
+ for url in urls:
+ url = url.strip()
+ if not url:
+ continue
+ self.add_feed(url)
+
+ def enable(self):
+ """[summary]
+ """
+
+ self.icon.set_icon('icons/feed.png')
+ self.enabled = True
+ self.poll()
+
+ def disable(self):
+ """[summary]
+ """
+
+ self.icon.set_icon('icons/feed_disabled.png')
+ self.enabled = False
+
+ def save(self):
+ """[summary]
+ """
+
+ self.manager.save()
+
+ def on_check_for_updates(self):
+ """[summary]
+ """
+
+ try:
+ self.check_for_updates(False)
+ finally:
+ wx.CallLater(1000 * 60 * 5, self.on_check_for_updates)
+
+ def check_for_updates(self, force=True):
+ """[summary]
+
+ Keyword Arguments:
+ force {bool} -- [description] (default: {True})
+ """
+
+ updater.run(self, force)
+
+ def on_poll(self):
+ """[summary]
+ """
+
+ try:
+ self.poll()
+ finally:
+ wx.CallLater(1000 * 5, self.on_poll)
+
+ def poll(self):
+ """[summary]
+ """
+
+ if self.polling:
+ return
+ if not self.enabled:
+ return
+ if settings.DISABLE_WHEN_IDLE and idle.get_idle_duration() > \
+ settings.USER_IDLE_TIMEOUT:
+ return
+ if not self.manager.should_poll():
+ return
+ self.polling = True
+ self.icon.set_icon('icons/feed_go.png')
+ util.start_thread(self._poll_thread)
+
+ def _poll_thread(self):
+ """[summary]
+ """
+
+ found_new = False
+ try:
+ for new_items in self.manager.poll():
+ found_new = True
+ wx.CallAfter(self._poll_result, new_items)
+ finally:
+ wx.CallAfter(self._poll_complete, found_new)
+
+ def _poll_result(self, new_items):
+ """[summary]
+
+ Arguments:
+ new_items {[type]} -- [description]
+ """
+
+ items = self.manager.items
+ if self.popup:
+ index = self.popup.index
+ else:
+ index = len(items)
+ items.extend(new_items)
+ self.show_items(items, index, False)
+
+ def _poll_complete(self, found_new):
+ """[summary]
+
+ Arguments:
+ found_new {[type]} -- [description]
+ """
+ if found_new:
+ self.save()
+ self.polling = False
+ self.icon.set_icon('icons/feed.png')
+
+ def force_poll(self):
+ """[summary]
+ """
+
+ for feed in self.manager.feeds:
+ feed.last_poll = 0
+ self.poll()
+
+ def show_items(self, items, index, focus):
+ """[summary]
+
+ Arguments:
+ items {[type]} -- [description]
+ index {[type]} -- [description]
+ focus {[type]} -- [description]
+ """
+
+ play_sound = False
+ if not items:
+ return
+ if not self.popup:
+ self.popup = popups.PopupManager()
+ self.popup.Bind(popups.EVT_POPUP_CLOSE, self.on_popup_close)
+ if not focus:
+ play_sound = True
+ self.popup.set_items(items, index, focus)
+ if focus:
+ self.popup.auto = False
+ if play_sound:
+ self.play_sound()
+
+ def play_sound(self):
+ """[summary]
+ """
+
+ if settings.PLAY_SOUND:
+ path = settings.SOUND_PATH
+ flags = winsound.SND_FILENAME | winsound.SND_ASYNC
+ try:
+ winsound.PlaySound(path, flags)
+ except Exception:
+ pass
+
+ def show_popup(self):
+ """[summary]
+ """
+ items = self.manager.items
+ index = len(items) - 1
+ self.show_items(items, index, True)
+
+ def add_feed(self, url=''):
+ """[summary]
+
+ Keyword Arguments:
+ url {str} -- [description] (default: {''})
+ """
+
+ feed = view.AddFeedDialog.show_wizard(None, url)
+ if not feed:
+ return
+ self.manager.add_feed(feed)
+ self.save()
+ self.poll()
+
+ def edit_settings(self):
+ """[summary]
+ """
+
+ window = view.SettingsDialog(None, self)
+ window.Center()
+ window.ShowModal()
+ window.Destroy()
+
+ def close(self):
+ """[summary]
+ """
+
+ try:
+ if self.popup:
+ self.popup.on_close()
+ wx.CallAfter(self.icon.Destroy)
+ finally:
+ pass # wx.GetApp().ExitMainLoop()
+
+ def on_popup_close(self, event):
+ self.popup = None
+ self.manager.purge_items(settings.ITEM_CACHE_AGE)
+
+# EOF
diff --git a/controls.py b/controls.py
index dec9a25..9eba171 100644
--- a/controls.py
+++ b/controls.py
@@ -1,192 +1,451 @@
-import wx
-import wx.lib.wordwrap as wordwrap
-import util
-
-class Event(wx.PyEvent):
- def __init__(self, event_object, type):
- super(Event, self).__init__()
- self.SetEventType(type.typeId)
- self.SetEventObject(event_object)
-
-EVT_HYPERLINK = wx.PyEventBinder(wx.NewEventType())
-
-class Line(wx.PyPanel):
- def __init__(self, parent, pen=wx.BLACK_PEN):
- super(Line, self).__init__(parent, -1, style=wx.BORDER_NONE)
- self.pen = pen
- self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
- self.Bind(wx.EVT_PAINT, self.on_paint)
- self.Bind(wx.EVT_SIZE, self.on_size)
- def on_size(self, event):
- self.Refresh()
- def on_paint(self, event):
- dc = wx.AutoBufferedPaintDC(self)
- dc.Clear()
- dc.SetPen(self.pen)
- width, height = self.GetClientSize()
- y = height / 2
- dc.DrawLine(0, y, width, y)
- def DoGetBestSize(self):
- return -1, self.pen.GetWidth()
-
-class Text(wx.PyPanel):
- def __init__(self, parent, width, text):
- super(Text, self).__init__(parent, -1, style=wx.BORDER_NONE)
- self.text = text
- self.width = width
- self.wrap = True
- self.rects = []
- self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
- self.Bind(wx.EVT_PAINT, self.on_paint)
- self.Bind(wx.EVT_SIZE, self.on_size)
- def on_size(self, event):
- self.Refresh()
- def on_paint(self, event):
- dc = wx.AutoBufferedPaintDC(self)
- self.setup_dc(dc)
- dc.Clear()
- self.draw_lines(dc)
- def setup_dc(self, dc):
- parent = self.GetParent()
- dc.SetFont(self.GetFont())
- dc.SetTextBackground(parent.GetBackgroundColour())
- dc.SetTextForeground(parent.GetForegroundColour())
- dc.SetBackground(wx.Brush(parent.GetBackgroundColour()))
- def draw_lines(self, dc, emulate=False):
- if self.wrap:
- text = wordwrap.wordwrap(self.text.strip(), self.width, dc)
- else:
- text = self.text.strip()
- lines = text.split('\n')
- lines = [line.strip() for line in lines]
- lines = [line for line in lines if line]
- x, y = 0, 0
- rects = []
- for line in lines:
- if not emulate:
- dc.DrawText(line, x, y)
- w, h = dc.GetTextExtent(line)
- rects.append(wx.Rect(x, y, w, h))
- y += h
- if not emulate:
- self.rects = rects
- return y
- def compute_height(self):
- dc = wx.ClientDC(self)
- self.setup_dc(dc)
- height = self.draw_lines(dc, True)
- return height
- def fit_no_wrap(self):
- dc = wx.ClientDC(self)
- self.setup_dc(dc)
- width, height = dc.GetTextExtent(self.text.strip())
- self.width = width
- self.wrap = False
- def DoGetBestSize(self):
- height = self.compute_height()
- return self.width, height
-
-class Link(Text):
- def __init__(self, parent, width, link, text):
- super(Link, self).__init__(parent, width, text)
- self.link = link
- self.trigger = False
- self.hover = False
- self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
- self.Bind(wx.EVT_MOTION, self.on_motion)
- self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
- self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
- self.Bind(wx.EVT_RIGHT_UP, self.on_right_up)
- def hit_test(self, point):
- for rect in self.rects:
- if rect.Contains(point):
- self.on_hover()
- break
- else:
- self.on_unhover()
- def on_motion(self, event):
- self.hit_test(event.GetPosition())
- def on_leave(self, event):
- self.on_unhover()
- def on_hover(self):
- if self.hover:
- return
- self.hover = True
- font = self.GetFont()
- font.SetUnderlined(True)
- self.SetFont(font)
- self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
- self.Refresh()
- def on_unhover(self):
- if not self.hover:
- return
- self.hover = False
- self.trigger = False
- font = self.GetFont()
- font.SetUnderlined(False)
- self.SetFont(font)
- self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
- self.Refresh()
- def on_left_down(self, event):
- if self.hover:
- self.trigger = True
- def on_left_up(self, event):
- if self.hover and self.trigger:
- self.post_event()
- self.trigger = False
- def on_right_up(self, event):
- menu = wx.Menu()
- util.menu_item(menu, 'Open Link', self.on_open_link)
- util.menu_item(menu, 'Copy Link', self.on_copy_link)
- self.PopupMenu(menu, event.GetPosition())
- def on_open_link(self, event):
- self.post_event()
- def on_copy_link(self, event):
- if wx.TheClipboard.Open():
- wx.TheClipboard.SetData(wx.TextDataObject(self.link))
- wx.TheClipboard.Close()
- def post_event(self):
- event = Event(self, EVT_HYPERLINK)
- event.link = self.link
- wx.PostEvent(self, event)
-
-class BitmapLink(wx.PyPanel):
- def __init__(self, parent, link, bitmap, hover_bitmap=None):
- super(BitmapLink, self).__init__(parent, -1)
- self.link = link
- self.bitmap = bitmap
- self.hover_bitmap = hover_bitmap or bitmap
- self.hover = False
- self.trigger = False
- self.SetInitialSize(bitmap.GetSize())
- self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
- self.Bind(wx.EVT_PAINT, self.on_paint)
- self.Bind(wx.EVT_ENTER_WINDOW, self.on_enter)
- self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
- self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
- self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
- def on_paint(self, event):
- parent = self.GetParent()
- dc = wx.AutoBufferedPaintDC(self)
- dc.SetBackground(wx.Brush(parent.GetBackgroundColour()))
- dc.Clear()
- bitmap = self.hover_bitmap if self.hover else self.bitmap
- dc.DrawBitmap(bitmap, 0, 0, True)
- def on_enter(self, event):
- self.hover = True
- self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
- self.Refresh()
- def on_leave(self, event):
- self.trigger = False
- self.hover = False
- self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
- self.Refresh()
- def on_left_down(self, event):
- self.trigger = True
- def on_left_up(self, event):
- if self.trigger:
- event = Event(self, EVT_HYPERLINK)
- event.link = self.link
- wx.PostEvent(self, event)
- self.trigger = False
-
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+"""[summary]
+
+Returns:
+ [type] -- [description]
+"""
+
+
+import wx
+import wx.lib.wordwrap as wordwrap
+
+import util
+
+
+class Event(wx.PyEvent):
+ """[summary]
+
+ Arguments:
+ wx {[type]} -- [description]
+ """
+
+ def __init__(self, event_object, type):
+ """[summary]
+
+ Arguments:
+ event_object {[type]} -- [description]
+ type {[type]} -- [description]
+ """
+
+ super(Event, self).__init__()
+ self.SetEventType(type.typeId)
+ self.SetEventObject(event_object)
+
+
+EVT_HYPERLINK = wx.PyEventBinder(wx.NewEventType())
+
+
+class Line(wx.PyPanel):
+ """[summary]
+
+ Arguments:
+ wx {[type]} -- [description]
+ """
+
+ def __init__(self, parent, pen=wx.BLACK_PEN):
+ """[summary]
+
+ Arguments:
+ parent {[type]} -- [description]
+
+ Keyword Arguments:
+ pen {[type]} -- [description] (default: {wx.BLACK_PEN})
+ """
+
+ super(Line, self).__init__(parent, -1, style=wx.BORDER_NONE)
+ self.pen = pen
+ self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
+ self.Bind(wx.EVT_PAINT, self.on_paint)
+ self.Bind(wx.EVT_SIZE, self.on_size)
+
+ def on_size(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.Refresh()
+
+ def on_paint(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ dc = wx.AutoBufferedPaintDC(self)
+ dc.Clear()
+ dc.SetPen(self.pen)
+ width, height = self.GetClientSize()
+ y = height / 2
+ dc.DrawLine(0, y, width, y)
+
+ def DoGetBestSize(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return -1, self.pen.GetWidth()
+
+
+class Text(wx.PyPanel):
+ """[summary]
+
+ Arguments:
+ wx {[type]} -- [description]
+ """
+
+ def __init__(self, parent, width, text):
+ """[summary]
+
+ Arguments:
+ parent {[type]} -- [description]
+ width {[type]} -- [description]
+ text {[type]} -- [description]
+ """
+
+ super(Text, self).__init__(parent, -1, style=wx.BORDER_NONE)
+ self.text = text
+ self.width = width
+ self.wrap = True
+ self.rects = []
+ self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
+ self.Bind(wx.EVT_PAINT, self.on_paint)
+ self.Bind(wx.EVT_SIZE, self.on_size)
+
+ def on_size(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.Refresh()
+
+ def on_paint(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ dc = wx.AutoBufferedPaintDC(self)
+ self.setup_dc(dc)
+ dc.Clear()
+ self.draw_lines(dc)
+
+ def setup_dc(self, dc):
+ """[summary]
+
+ Arguments:
+ dc {[type]} -- [description]
+ """
+
+ parent = self.GetParent()
+ dc.SetFont(self.GetFont())
+ dc.SetTextBackground(parent.GetBackgroundColour())
+ dc.SetTextForeground(parent.GetForegroundColour())
+ dc.SetBackground(wx.Brush(parent.GetBackgroundColour()))
+
+ def draw_lines(self, dc, emulate=False):
+ """[summary]
+
+ Arguments:
+ dc {[type]} -- [description]
+
+ Keyword Arguments:
+ emulate {bool} -- [description] (default: {False})
+
+ Returns:
+ [type] -- [description]
+ """
+
+ if self.wrap:
+ text = wordwrap.wordwrap(self.text.strip(), self.width, dc)
+ else:
+ text = self.text.strip()
+ lines = text.split('\n')
+ lines = [line.strip() for line in lines]
+ lines = [line for line in lines if line]
+ x, y = 0, 0
+ rects = []
+ for line in lines:
+ if not emulate:
+ dc.DrawText(line, x, y)
+ w, h = dc.GetTextExtent(line)
+ rects.append(wx.Rect(x, y, w, h))
+ y += h
+ if not emulate:
+ self.rects = rects
+ return y
+
+ def compute_height(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ dc = wx.ClientDC(self)
+ self.setup_dc(dc)
+ height = self.draw_lines(dc, True)
+ return height
+
+ def fit_no_wrap(self):
+ """[summary]
+ """
+
+ dc = wx.ClientDC(self)
+ self.setup_dc(dc)
+ width, height = dc.GetTextExtent(self.text.strip())
+ self.width = width
+ self.wrap = False
+
+ def DoGetBestSize(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ height = self.compute_height()
+ return self.width, height
+
+
+class Link(Text):
+ """[summary]
+
+ Arguments:
+ Text {[type]} -- [description]
+ """
+
+ def __init__(self, parent, width, link, text):
+ super(Link, self).__init__(parent, width, text)
+ self.link = link
+ self.trigger = False
+ self.hover = False
+ self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
+ self.Bind(wx.EVT_MOTION, self.on_motion)
+ self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
+ self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
+ self.Bind(wx.EVT_RIGHT_UP, self.on_right_up)
+
+ def hit_test(self, point):
+ """[summary]
+
+ Arguments:
+ point {[type]} -- [description]
+ """
+
+ for rect in self.rects:
+ if rect.Contains(point):
+ self.on_hover()
+ break
+ else:
+ self.on_unhover()
+
+ def on_motion(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.hit_test(event.GetPosition())
+
+ def on_leave(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.on_unhover()
+
+ def on_hover(self):
+ """[summary]
+ """
+
+ if self.hover:
+ return
+ self.hover = True
+ font = self.GetFont()
+ font.SetUnderlined(True)
+ self.SetFont(font)
+ self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
+ self.Refresh()
+
+ def on_unhover(self):
+ """[summary]
+ """
+
+ if not self.hover:
+ return
+ self.hover = False
+ self.trigger = False
+ font = self.GetFont()
+ font.SetUnderlined(False)
+ self.SetFont(font)
+ self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
+ self.Refresh()
+
+ def on_left_down(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ if self.hover:
+ self.trigger = True
+
+ def on_left_up(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ if self.hover and self.trigger:
+ self.post_event()
+
+ self.trigger = False
+
+ def on_right_up(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ menu = wx.Menu()
+ util.menu_item(menu, 'Open Link', self.on_open_link)
+ util.menu_item(menu, 'Copy Link', self.on_copy_link)
+ self.PopupMenu(menu, event.GetPosition())
+
+ def on_open_link(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.post_event()
+
+ def on_copy_link(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ if wx.TheClipboard.Open():
+ wx.TheClipboard.SetData(wx.TextDataObject(self.link))
+ wx.TheClipboard.Close()
+
+ def post_event(self):
+ """[summary]
+ """
+
+ event = Event(self, EVT_HYPERLINK)
+ event.link = self.link
+ wx.PostEvent(self, event)
+
+
+class BitmapLink(wx.PyPanel):
+ """[summary]
+
+ Arguments:
+ wx {[type]} -- [description]
+ """
+
+ def __init__(self, parent, link, bitmap, hover_bitmap=None):
+ """[summary]
+
+ Arguments:
+ parent {[type]} -- [description]
+ link {[type]} -- [description]
+ bitmap {[type]} -- [description]
+
+ Keyword Arguments:
+ hover_bitmap {[type]} -- [description] (default: {None})
+ """
+
+ super(BitmapLink, self).__init__(parent, -1)
+ self.link = link
+ self.bitmap = bitmap
+ self.hover_bitmap = hover_bitmap or bitmap
+ self.hover = False
+ self.trigger = False
+ self.SetInitialSize(bitmap.GetSize())
+ self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
+ self.Bind(wx.EVT_PAINT, self.on_paint)
+ self.Bind(wx.EVT_ENTER_WINDOW, self.on_enter)
+ self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
+ self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
+ self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
+
+ def on_paint(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ parent = self.GetParent()
+ dc = wx.AutoBufferedPaintDC(self)
+ dc.SetBackground(wx.Brush(parent.GetBackgroundColour()))
+ dc.Clear()
+ bitmap = self.hover_bitmap if self.hover else self.bitmap
+ dc.DrawBitmap(bitmap, 0, 0, True)
+
+ def on_enter(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.hover = True
+ self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
+ self.Refresh()
+
+ def on_leave(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.trigger = False
+ self.hover = False
+ self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
+ self.Refresh()
+
+ def on_left_down(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ self.trigger = True
+
+ def on_left_up(self, event):
+ """[summary]
+
+ Arguments:
+ event {[type]} -- [description]
+ """
+
+ if self.trigger:
+ event = Event(self, EVT_HYPERLINK)
+ event.link = self.link
+ wx.PostEvent(self, event)
+ self.trigger = False
+
+# EOF
\ No newline at end of file
diff --git a/defaults.py b/defaults.py
index 6694d8b..6ef5498 100644
--- a/defaults.py
+++ b/defaults.py
@@ -1,57 +1,72 @@
-# Helper Functions
-def load_revision():
- try:
- with open('revision.txt', 'r') as file:
- return int(file.read().strip())
- except Exception:
- return -1
-
-# Popup Settings
-POPUP_DURATION = 5
-POPUP_AUTO_PLAY = True
-POPUP_WAIT_ON_HOVER = True
-POPUP_THEME = 'default'
-POPUP_WIDTH = 400
-POPUP_POSITION = (1, 1)
-POPUP_TRANSPARENCY = 230
-POPUP_TITLE_LENGTH = 120
-POPUP_BODY_LENGTH = 400
-POPUP_DISPLAY = 0
-POPUP_STAY_ON_TOP = True
-POPUP_BORDER_SIZE = 3
-POPUP_BORDER_COLOR = (0, 0, 0)
-
-# Application Settings
-APP_ID = 'FeedNotifier'
-APP_NAME = 'Feed Notifier'
-APP_VERSION = '2.6'
-APP_URL = 'http://www.feednotifier.com/'
-USER_AGENT = '%s/%s +%s' % (APP_ID, APP_VERSION, APP_URL)
-DEFAULT_POLLING_INTERVAL = 60 * 15
-USER_IDLE_TIMEOUT = 60
-DISABLE_WHEN_IDLE = True
-ITEM_CACHE_AGE = 60 * 60 * 24 * 1
-FEED_CACHE_SIZE = 1000
-MAX_WORKER_THREADS = 10
-PLAY_SOUND = True
-SOUND_PATH = 'sounds/notification.wav'
-SOCKET_TIMEOUT = 15
-
-# Initial Setup
-DEFAULT_FEED_URLS = [
- 'http://www.feednotifier.com/welcome.xml',
-]
-
-# Proxy Settings
-USE_PROXY = False
-PROXY_URL = ''
-
-# Updater Settings
-LOCAL_REVISION = load_revision()
-REVISION_URL = 'http://www.feednotifier.com/update/revision.txt'
-INSTALLER_URL = 'http://www.feednotifier.com/update/installer.exe'
-CHECK_FOR_UPDATES = True
-UPDATE_INTERVAL = 60 * 60 * 24 * 1
-UPDATE_TIMESTAMP = 0
-
-del load_revision
+# -*- coding: utf-8 -*-
+
+"""[summary]
+
+Returns:
+ [type] -- [description]
+"""
+
+import logging
+
+
+# Helper Functions
+def load_revision():
+ try:
+ with open('revision.txt', 'r') as file:
+ logging.info(f'Reading the file revision.txt.')
+ return int(file.read().strip())
+ except Exception:
+ logging.error(f'The file revision.txt does not exist.')
+ return -1
+
+
+# Popup Settings
+POPUP_DURATION = 5
+POPUP_AUTO_PLAY = True
+POPUP_WAIT_ON_HOVER = True
+POPUP_THEME = 'default'
+POPUP_WIDTH = 400
+POPUP_POSITION = (1, 1)
+POPUP_TRANSPARENCY = 230
+POPUP_TITLE_LENGTH = 120
+POPUP_BODY_LENGTH = 400
+POPUP_DISPLAY = 0
+POPUP_STAY_ON_TOP = True
+POPUP_BORDER_SIZE = 3
+POPUP_BORDER_COLOR = (0, 0, 0)
+
+# Application Settings
+APP_ID = 'FeedNotifier'
+APP_NAME = 'Feed Notifier'
+APP_VERSION = '2.6'
+APP_URL = 'http://www.feednotifier.com/'
+USER_AGENT = '%s/%s +%s' % (APP_ID, APP_VERSION, APP_URL)
+DEFAULT_POLLING_INTERVAL = 60 * 15
+USER_IDLE_TIMEOUT = 60
+DISABLE_WHEN_IDLE = True
+ITEM_CACHE_AGE = 60 * 60 * 24 * 1
+FEED_CACHE_SIZE = 1000
+MAX_WORKER_THREADS = 10
+PLAY_SOUND = True
+SOUND_PATH = 'sounds/notification.wav'
+SOCKET_TIMEOUT = 15
+
+# Initial Setup
+DEFAULT_FEED_URLS = [
+ 'http://www.feednotifier.com/welcome.xml',
+ 'https://www.pic-sl.com/unycop-beta.xml',
+]
+
+# Proxy Settings
+USE_PROXY = False
+PROXY_URL = ''
+
+# Updater Settings
+LOCAL_REVISION = load_revision()
+REVISION_URL = 'http://www.feednotifier.com/update/revision.txt'
+INSTALLER_URL = 'http://www.feednotifier.com/update/installer.exe'
+CHECK_FOR_UPDATES = True
+UPDATE_INTERVAL = 60 * 60 * 24 * 1
+UPDATE_TIMESTAMP = 0
+
+del load_revision
diff --git a/doc/FeedNotifier.png b/doc/FeedNotifier.png
new file mode 100644
index 0000000..af3400f
Binary files /dev/null and b/doc/FeedNotifier.png differ
diff --git a/doc/UML.png b/doc/UML.png
new file mode 100644
index 0000000..144b840
Binary files /dev/null and b/doc/UML.png differ
diff --git a/doc/UML.svg b/doc/UML.svg
new file mode 100644
index 0000000..cca09d3
--- /dev/null
+++ b/doc/UML.svg
@@ -0,0 +1,39975 @@
+
+
diff --git a/doc/pycallgrap-FeedNotifier-Windows.bat b/doc/pycallgrap-FeedNotifier-Windows.bat
new file mode 100644
index 0000000..3cd2407
--- /dev/null
+++ b/doc/pycallgrap-FeedNotifier-Windows.bat
@@ -0,0 +1 @@
+pycallgraph -s -t --max-depth 14 graphviz --output-file=FeedNotifier.png ..\main.py
diff --git a/doc/pycallgraph_-t_--max-depth_14_graphviz_--_main.py.png b/doc/pycallgraph_-t_--max-depth_14_graphviz_--_main.py.png
new file mode 100644
index 0000000..7ad4e9c
Binary files /dev/null and b/doc/pycallgraph_-t_--max-depth_14_graphviz_--_main.py.png differ
diff --git a/dummy.py b/dummy.py
index b8fe5fe..47a453d 100644
--- a/dummy.py
+++ b/dummy.py
@@ -1 +1,11 @@
-# used for dummy.__file__ to setup path
+# -*- coding: utf-8 -*-
+
+"""[summary]
+
+Returns:
+ [type] -- [description]
+"""
+
+# used for dummy.__file__ to setup path
+
+# EOF
\ No newline at end of file
diff --git a/feeds.py b/feeds.py
index ef87bb2..28eb7f3 100644
--- a/feeds.py
+++ b/feeds.py
@@ -1,275 +1,590 @@
-import os
-import time
-import calendar
-import uuid
-import urlparse
-import urllib2
-import filters
-import util
-import Queue
-import logging
-import safe_pickle
-from settings import settings
-
-def cmp_timestamp(a, b):
- return cmp(a.timestamp, b.timestamp)
-
-def create_id(entry):
- keys = ['id', 'link', 'title']
- values = tuple(util.get(entry, key, None) for key in keys)
- return values if any(values) else uuid.uuid4().hex
-
-class Item(object):
- def __init__(self, feed, id):
- self.feed = feed
- self.id = id
- self.timestamp = int(time.time())
- self.received = int(time.time())
- self.title = ''
- self.description = ''
- self.link = ''
- self.author = ''
- self.read = False
- @property
- def time_since(self):
- return util.time_since(self.timestamp)
-
-class Feed(object):
- def __init__(self, url):
- self.uuid = uuid.uuid4().hex
- self.url = url
- self.username = None
- self.password = None
- self.enabled = True
- self.last_poll = 0
- self.interval = settings.DEFAULT_POLLING_INTERVAL
- self.etag = None
- self.modified = None
- self.title = ''
- self.link = ''
- self.clicks = 0
- self.item_count = 0
- self.color = None
- self.id_list = []
- self.id_set = set()
- def make_copy(self):
- feed = Feed(self.url)
- for key in ['uuid', 'enabled', 'interval', 'title', 'link', 'clicks', 'item_count', 'color']:
- value = getattr(self, key)
- setattr(feed, key, value)
- return feed
- def copy_from(self, feed):
- for key in ['enabled', 'interval', 'title', 'link', 'color']:
- value = getattr(feed, key)
- setattr(self, key, value)
- @property
- def favicon_url(self):
- components = urlparse.urlsplit(self.link)
- scheme, domain = components[:2]
- return '%s://%s/favicon.ico' % (scheme, domain)
- @property
- def favicon_path(self):
- components = urlparse.urlsplit(self.link)
- scheme, domain = components[:2]
- path = 'icons/cache/%s.ico' % domain
- return os.path.abspath(path)
- @property
- def has_favicon(self):
- return os.path.exists(self.favicon_path)
- def download_favicon(self):
- # make cache directory if needed
- try:
- dir, name = os.path.split(self.favicon_path)
- os.makedirs(dir)
- except Exception:
- pass
- # try to download the favicon
- try:
- opener = urllib2.build_opener(util.get_proxy())
- f = opener.open(self.favicon_url)
- data = f.read()
- f.close()
- f = open(self.favicon_path, 'wb')
- f.write(data)
- f.close()
- except Exception:
- pass
- def clear_cache(self):
- self.id_list = []
- self.id_set = set()
- self.etag = None
- self.modified = None
- def clean_cache(self, size):
- for id in self.id_list[:-size]:
- self.id_set.remove(id)
- self.id_list = self.id_list[-size:]
- def should_poll(self):
- if not self.enabled:
- return False
- now = int(time.time())
- duration = now - self.last_poll
- return duration >= self.interval
- def poll(self, timestamp, filters):
- logging.info('Polling feed "%s"' % self.url)
- result = []
- self.last_poll = timestamp
- username = util.decode_password(self.username)
- password = util.decode_password(self.password)
- d = util.parse(self.url, username, password, self.etag, self.modified)
- self.etag = util.get(d, 'etag', None)
- self.modified = util.get(d, 'modified', None)
- feed = util.get(d, 'feed', None)
- if feed:
- self.title = self.title or util.get(feed, 'title', '')
- self.link = self.link or util.get(feed, 'link', self.url)
- entries = util.get(d, 'entries', [])
- for entry in reversed(entries):
- id = create_id(entry)
- if id in self.id_set:
- continue
- self.item_count += 1
- self.id_list.append(id)
- self.id_set.add(id)
- item = Item(self, id)
- item.timestamp = calendar.timegm(util.get(entry, 'date_parsed', time.gmtime()))
- item.title = util.format(util.get(entry, 'title', ''), settings.POPUP_TITLE_LENGTH)
- item.description = util.format(util.get(entry, 'description', ''), settings.POPUP_BODY_LENGTH)
- item.link = util.get(entry, 'link', '')
- item.author = util.format(util.get(entry, 'author', '')) # TODO: max length
- if all(filter.filter(item) for filter in filters):
- result.append(item)
- self.clean_cache(settings.FEED_CACHE_SIZE)
- return result
-
-class Filter(object):
- def __init__(self, code, ignore_case=True, whole_word=True, feeds=None):
- self.uuid = uuid.uuid4().hex
- self.enabled = True
- self.code = code
- self.ignore_case = ignore_case
- self.whole_word = whole_word
- self.feeds = set(feeds) if feeds else set()
- self.inputs = 0
- self.outputs = 0
- def make_copy(self):
- filter = Filter(self.code, self.ignore_case, self.whole_word, self.feeds)
- for key in ['uuid', 'enabled', 'inputs', 'outputs']:
- value = getattr(self, key)
- setattr(filter, key, value)
- return filter
- def copy_from(self, filter):
- for key in ['enabled', 'code', 'ignore_case', 'whole_word', 'feeds']:
- value = getattr(filter, key)
- setattr(self, key, value)
- def filter(self, item):
- if not self.enabled:
- return True
- if self.feeds and item.feed not in self.feeds:
- return True
- self.inputs += 1
- rule = filters.parse(self.code) # TODO: cache parsed rules
- if rule.evaluate(item, self.ignore_case, self.whole_word):
- self.outputs += 1
- return True
- else:
- return False
-
-class FeedManager(object):
- def __init__(self):
- self.feeds = []
- self.items = []
- self.filters = []
- def add_feed(self, feed):
- logging.info('Adding feed "%s"' % feed.url)
- self.feeds.append(feed)
- def remove_feed(self, feed):
- logging.info('Removing feed "%s"' % feed.url)
- self.feeds.remove(feed)
- for filter in self.filters:
- filter.feeds.discard(feed)
- def add_filter(self, filter):
- logging.info('Adding filter "%s"' % filter.code)
- self.filters.append(filter)
- def remove_filter(self, filter):
- logging.info('Removing filter "%s"' % filter.code)
- self.filters.remove(filter)
- def should_poll(self):
- return any(feed.should_poll() for feed in self.feeds)
- def poll(self):
- now = int(time.time())
- jobs = Queue.Queue()
- results = Queue.Queue()
- feeds = [feed for feed in self.feeds if feed.should_poll()]
- for feed in feeds:
- jobs.put(feed)
- count = len(feeds)
- logging.info('Starting worker threads')
- for i in range(min(count, settings.MAX_WORKER_THREADS)):
- util.start_thread(self.worker, now, jobs, results)
- while count:
- items = results.get()
- count -= 1
- if items:
- yield items
- logging.info('Worker threads completed')
- def worker(self, now, jobs, results):
- while True:
- try:
- feed = jobs.get(False)
- except Queue.Empty:
- break
- try:
- items = feed.poll(now, self.filters)
- items.sort(cmp=cmp_timestamp)
- if items and not feed.has_favicon:
- feed.download_favicon()
- results.put(items)
- jobs.task_done()
- except Exception:
- results.put([])
- jobs.task_done()
- def purge_items(self, max_age):
- now = int(time.time())
- feeds = set(self.feeds)
- for item in list(self.items):
- age = now - item.received
- if age > max_age or item.feed not in feeds:
- self.items.remove(item)
- def load(self, path='feeds.dat'):
- logging.info('Loading feed data from "%s"' % path)
- try:
- data = safe_pickle.load(path)
- except Exception:
- data = ([], [], [])
- # backward compatibility
- if len(data) == 2:
- self.feeds, self.items = data
- self.filters = []
- else:
- self.feeds, self.items, self.filters = data
- attributes = {
- 'clicks': 0,
- 'item_count': 0,
- 'username': None,
- 'password': None,
- 'color': None,
- }
- for feed in self.feeds:
- for name, value in attributes.iteritems():
- if not hasattr(feed, name):
- setattr(feed, name, value)
- if not hasattr(feed, 'id_list'):
- feed.id_list = list(feed.id_set)
- logging.info('Loaded %d feeds, %d items, %d filters' % (len(self.feeds), len(self.items), len(self.filters)))
- def save(self, path='feeds.dat'):
- logging.info('Saving feed data to "%s"' % path)
- data = (self.feeds, self.items, self.filters)
- safe_pickle.save(path, data)
- def clear_item_history(self):
- logging.info('Clearing item history')
- del self.items[:]
- def clear_feed_cache(self):
- logging.info('Clearing feed caches')
- for feed in self.feeds:
- feed.clear_cache()
-
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+"""[summary]
+
+Returns:
+ [type] -- [description]
+"""
+
+
+import calendar
+import logging
+import os
+import queue
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+import uuid
+
+import filters
+import safe_pickle
+import util
+
+from settings import settings
+
+
+def cmp_timestamp(a, b):
+ """[summary]
+
+ Arguments:
+ a {[type]} -- [description]
+ b {[type]} -- [description]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return cmp(a.timestamp, b.timestamp)
+
+
+def create_id(entry):
+ """[summary]
+
+ Arguments:
+ entry {[type]} -- [description]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ keys = ['id', 'link', 'title']
+ values = tuple(util.get(entry, key, None) for key in keys)
+ return values if any(values) else uuid.uuid4().hex
+
+
+class Item(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self, feed, id):
+ """[summary]
+
+ Arguments:
+ feed {[type]} -- [description]
+ id {[type]} -- [description]
+ """
+
+ self.feed = feed
+ self.id = id
+ self.timestamp = int(time.time())
+ self.received = int(time.time())
+ self.title = ''
+ self.description = ''
+ self.link = ''
+ self.author = ''
+ self.read = False
+
+ @property
+ def time_since(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return util.time_since(self.timestamp)
+
+
+class Feed(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self, url):
+ """[summary]
+
+ Arguments:
+ url {[type]} -- [description]
+ """
+
+ self.uuid = uuid.uuid4().hex
+ self.url = url
+ self.username = None
+ self.password = None
+ self.enabled = True
+ self.last_poll = 0
+ self.interval = settings.DEFAULT_POLLING_INTERVAL
+ self.etag = None
+ self.modified = None
+ self.title = ''
+ self.link = ''
+ self.clicks = 0
+ self.item_count = 0
+ self.color = None
+ self.id_list = []
+ self.id_set = set()
+
+ def make_copy(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ feed = Feed(self.url)
+
+ for key in ['uuid', 'enabled', 'interval', 'title', 'link', 'clicks', 'item_count', 'color']:
+ value = getattr(self, key)
+ setattr(feed, key, value)
+
+ return feed
+
+ def copy_from(self, feed):
+ """[summary]
+
+ Arguments:
+ feed {[type]} -- [description]
+ """
+
+ for key in ['enabled', 'interval', 'title', 'link', 'color']:
+ value = getattr(feed, key)
+ setattr(self, key, value)
+
+ @property
+ def favicon_url(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ components = urllib.parse.urlsplit(self.link)
+ scheme, domain = components[:2]
+
+ return '%s://%s/favicon.ico' % (scheme, domain)
+
+ @property
+ def favicon_path(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ components = urllib.parse.urlsplit(self.link)
+ scheme, domain = components[:2]
+ path = 'icons/cache/%s.ico' % domain
+
+ return os.path.abspath(path)
+
+ @property
+ def has_favicon(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return os.path.exists(self.favicon_path)
+
+ def download_favicon(self):
+ """[summary]
+ """
+
+ # make cache directory if needed
+ try:
+ dir, name = os.path.split(self.favicon_path)
+ os.makedirs(dir)
+ except Exception:
+ pass
+
+ # try to download the favicon
+ try:
+ opener = urllib.request.build_opener(util.get_proxy())
+ f = opener.open(self.favicon_url)
+ data = f.read()
+ f.close()
+ f = open(self.favicon_path, 'wb')
+ f.write(data)
+ f.close()
+ except Exception:
+ pass
+
+ def clear_cache(self):
+ """[summary]
+ """
+
+ self.id_list = []
+ self.id_set = set()
+ self.etag = None
+ self.modified = None
+
+ def clean_cache(self, size):
+ """[summary]
+
+ Arguments:
+ size {[type]} -- [description]
+ """
+
+ for id in self.id_list[:-size]:
+ self.id_set.remove(id)
+ self.id_list = self.id_list[-size:]
+
+ def should_poll(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ if not self.enabled:
+ return False
+
+ now = int(time.time())
+ duration = now - self.last_poll
+
+ return duration >= self.interval
+
+ def poll(self, timestamp, filters):
+ """[summary]
+
+ Arguments:
+ timestamp {[type]} -- [description]
+ filters {[type]} -- [description]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ logging.info('Polling feed "%s"' % self.url)
+ result = []
+ self.last_poll = timestamp
+ username = util.decode_password(self.username)
+ password = util.decode_password(self.password)
+ d = util.parse(self.url, username, password, self.etag, self.modified)
+ self.etag = util.get(d, 'etag', None)
+ self.modified = util.get(d, 'modified', None)
+ feed = util.get(d, 'feed', None)
+
+ if feed:
+ self.title = self.title or util.get(feed, 'title', '')
+ self.link = self.link or util.get(feed, 'link', self.url)
+
+ entries = util.get(d, 'entries', [])
+
+ for entry in reversed(entries):
+ id = create_id(entry)
+ if id in self.id_set:
+ continue
+ self.item_count += 1
+ self.id_list.append(id)
+ self.id_set.add(id)
+ item = Item(self, id)
+ item.timestamp = calendar.timegm(
+ util.get(entry, 'date_parsed', time.gmtime()))
+ item.title = util.format(
+ util.get(entry, 'title', ''), settings.POPUP_TITLE_LENGTH)
+ item.description = util.format(
+ util.get(entry, 'description', ''), settings.POPUP_BODY_LENGTH)
+ item.link = util.get(entry, 'link', '')
+ item.author = util.format(
+ util.get(entry, 'author', '')) # TODO: max length
+ if all(filter.filter(item) for filter in filters):
+ result.append(item)
+
+ self.clean_cache(settings.FEED_CACHE_SIZE)
+
+ return result
+
+
+class Filter(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self, code, ignore_case=True, whole_word=True, feeds=None):
+ """[summary]
+
+ Arguments:
+ code {[type]} -- [description]
+
+ Keyword Arguments:
+ ignore_case {bool} -- [description] (default: {True})
+ whole_word {bool} -- [description] (default: {True})
+ feeds {[type]} -- [description] (default: {None})
+ """
+
+ self.uuid = uuid.uuid4().hex
+ self.enabled = True
+ self.code = code
+ self.ignore_case = ignore_case
+ self.whole_word = whole_word
+ self.feeds = set(feeds) if feeds else set()
+ self.inputs = 0
+ self.outputs = 0
+
+ def make_copy(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ filter = Filter(self.code, self.ignore_case,
+ self.whole_word, self.feeds)
+
+ for key in ['uuid', 'enabled', 'inputs', 'outputs']:
+ value = getattr(self, key)
+ setattr(filter, key, value)
+
+ return filter
+
+ def copy_from(self, filter):
+ """[summary]
+
+ Arguments:
+ filter {[type]} -- [description]
+ """
+
+ for key in ['enabled', 'code', 'ignore_case', 'whole_word', 'feeds']:
+ value = getattr(filter, key)
+ setattr(self, key, value)
+
+ def filter(self, item):
+ """[summary]
+
+ Arguments:
+ item {[type]} -- [description]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ if not self.enabled:
+ return True
+
+ if self.feeds and item.feed not in self.feeds:
+ return True
+
+ self.inputs += 1
+ rule = filters.parse(self.code) # TODO: cache parsed rules
+
+ if rule.evaluate(item, self.ignore_case, self.whole_word):
+ self.outputs += 1
+ return True
+ else:
+ return False
+
+
+class FeedManager(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self):
+ """Initializing FeedManager class
+ """
+
+ logging.debug(f'Initializing FeedManager class')
+
+ self.feeds = []
+ self.items = []
+ self.filters = []
+
+ logging.debug(f'Initialized FeedManager class')
+
+ def add_feed(self, feed):
+ """[summary]
+
+ Arguments:
+ feed {[type]} -- [description]
+ """
+
+ logging.info('Adding feed "%s"' % feed.url)
+
+ self.feeds.append(feed)
+
+ def remove_feed(self, feed):
+ """[summary]
+
+ Arguments:
+ feed {[type]} -- [description]
+ """
+
+ logging.info('Removing feed "%s"' % feed.url)
+
+ self.feeds.remove(feed)
+
+ for filter in self.filters:
+ filter.feeds.discard(feed)
+
+ def add_filter(self, filter):
+ """[summary]
+
+ Arguments:
+ filter {[type]} -- [description]
+ """
+
+ logging.info('Adding new filter "%s"' % filter.code)
+
+ self.filters.append(filter)
+
+ def remove_filter(self, filter):
+ """[summary]
+
+ Arguments:
+ filter {[type]} -- [description]
+ """
+
+ logging.info('Removing filter "%s"' % filter.code)
+
+ self.filters.remove(filter)
+
+ def should_poll(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ logging.info(f'Should poll each feed')
+
+ return any(feed.should_poll() for feed in self.feeds)
+
+ def poll(self):
+ """[summary]
+
+ Yields:
+ [type] -- [description]
+ """
+
+ logging.debug(f'')
+ now = int(time.time())
+ jobs = queue.Queue()
+ results = queue.Queue()
+ feeds = [feed for feed in self.feeds if feed.should_poll()]
+
+ for feed in feeds:
+ jobs.put(feed)
+
+ count = len(feeds)
+ logging.info('Starting worker threads')
+
+ for i in range(min(count, settings.MAX_WORKER_THREADS)):
+ util.start_thread(self.worker, now, jobs, results)
+
+ while count:
+ items = results.get()
+ count -= 1
+ if items:
+ yield items
+
+ logging.info('Worker threads completed')
+
+ def worker(self, now, jobs, results):
+ """[summary]
+
+ Arguments:
+ now {[type]} -- [description]
+ jobs {[type]} -- [description]
+ results {[type]} -- [description]
+ """
+
+ while True:
+ try:
+ feed = jobs.get(False)
+ except queue.Empty:
+ break
+ try:
+ items = feed.poll(now, self.filters)
+ items.sort(cmp=cmp_timestamp)
+ if items and not feed.has_favicon:
+ feed.download_favicon()
+ results.put(items)
+ jobs.task_done()
+ except Exception:
+ results.put([])
+ jobs.task_done()
+
+ def purge_items(self, max_age):
+ """[summary]
+
+ Arguments:
+ max_age {[type]} -- [description]
+ """
+
+ now = int(time.time())
+ feeds = set(self.feeds)
+
+ for item in list(self.items):
+ age = now - item.received
+ if age > max_age or item.feed not in feeds:
+ self.items.remove(item)
+
+ def load(self, path='feeds.dat'):
+ """[summary]
+
+ Keyword Arguments:
+ path {str} -- [description] (default: {'feeds.dat'})
+ """
+
+ logging.info('Loading feed data from "%s"' % path)
+
+ try:
+ data = safe_pickle.load(path)
+ except Exception:
+ data = ([], [], [])
+
+ # backward compatibility
+ if len(data) == 2:
+ self.feeds, self.items = data
+ self.filters = []
+ else:
+ self.feeds, self.items, self.filters = data
+
+ attributes = {
+ 'clicks': 0,
+ 'item_count': 0,
+ 'username': None,
+ 'password': None,
+ 'color': None,
+ }
+
+ for feed in self.feeds:
+ for name, value in list(attributes.items()):
+ if not hasattr(feed, name):
+ setattr(feed, name, value)
+ if not hasattr(feed, 'id_list'):
+ feed.id_list = list(feed.id_set)
+
+ logging.info('Loaded %d feeds, %d items, %d filters' %
+ (len(self.feeds), len(self.items), len(self.filters)))
+
+ def save(self, path='feeds.dat'):
+ """[summary]
+
+ Keyword Arguments:
+ path {str} -- [description] (default: {'feeds.dat'})
+ """
+
+ logging.info('Saving feed data to "%s"' % path)
+ data = (self.feeds, self.items, self.filters)
+ safe_pickle.save(path, data)
+
+ def clear_item_history(self):
+ """[summary]
+ """
+
+ logging.info('Clearing item history')
+ del self.items[:]
+
+ def clear_feed_cache(self):
+ """[summary]
+ """
+
+ logging.info('Clearing feed caches')
+
+ for feed in self.feeds:
+ feed.clear_cache()
+
+# EOF
\ No newline at end of file
diff --git a/filters.py b/filters.py
index 40da2d4..7d55a4f 100644
--- a/filters.py
+++ b/filters.py
@@ -1,223 +1,401 @@
-# Keyword Filter Parser
-
-EXCLUDE = 0
-INCLUDE = 1
-
-ALL = 0xf
-TITLE = 1
-LINK = 2
-AUTHOR = 4
-CONTENT = 8
-
-TYPES = {
- None: INCLUDE,
- '+': INCLUDE,
- '-': EXCLUDE,
-}
-
-QUALIFIERS = {
- None: ALL,
- 'title:': TITLE,
- 'link:': LINK,
- 'author:': AUTHOR,
- 'content:': CONTENT,
-}
-
-TYPE_STR = {
- EXCLUDE: '-',
- INCLUDE: '+',
-}
-
-QUALIFIER_STR = {
- ALL: 'all',
- TITLE: 'title',
- LINK: 'link',
- AUTHOR: 'author',
- CONTENT: 'content',
-}
-
-class Rule(object):
- def __init__(self, type, qualifier, word):
- self.type = TYPES.get(type, type)
- self.qualifier = QUALIFIERS.get(qualifier, qualifier)
- self.word = word
- def evaluate(self, item, ignore_case=True, whole_word=True):
- strings = []
- if self.qualifier & TITLE:
- strings.append(item.title)
- if self.qualifier & LINK:
- strings.append(item.link)
- if self.qualifier & AUTHOR:
- strings.append(item.author)
- if self.qualifier & CONTENT:
- strings.append(item.description)
- text = '\n'.join(strings)
- word = self.word
- if ignore_case:
- text = text.lower()
- word = word.lower()
- if whole_word:
- text = set(text.split())
- if word in text:
- return self.type == INCLUDE
- else:
- return self.type == EXCLUDE
- def __str__(self):
- type = TYPE_STR[self.type]
- qualifier = QUALIFIER_STR[self.qualifier]
- return '(%s, %s, "%s")' % (type, qualifier, self.word)
-
-class AndRule(object):
- def __init__(self, left, right):
- self.left = left
- self.right = right
- def evaluate(self, item, ignore_case=True, whole_word=True):
- a = self.left.evaluate(item, ignore_case, whole_word)
- b = self.right.evaluate(item, ignore_case, whole_word)
- return a and b
- def __str__(self):
- return '(%s and %s)' % (self.left, self.right)
-
-class OrRule(object):
- def __init__(self, left, right):
- self.left = left
- self.right = right
- def evaluate(self, item, ignore_case=True, whole_word=True):
- a = self.left.evaluate(item, ignore_case, whole_word)
- b = self.right.evaluate(item, ignore_case, whole_word)
- return a or b
- def __str__(self):
- return '(%s or %s)' % (self.left, self.right)
-
-class NotRule(object):
- def __init__(self, rule):
- self.rule = rule
- def evaluate(self, item, ignore_case=True, whole_word=True):
- return not self.rule.evaluate(item, ignore_case, whole_word)
- def __str__(self):
- return '(not %s)' % (self.rule)
-
-# Lexer Rules
-reserved = {
- 'and': 'AND',
- 'or': 'OR',
- 'not': 'NOT',
-}
-
-tokens = [
- 'PLUS',
- 'MINUS',
- 'LPAREN',
- 'RPAREN',
- 'TITLE',
- 'LINK',
- 'AUTHOR',
- 'CONTENT',
- 'WORD',
-] + reserved.values()
-
-t_PLUS = r'\+'
-t_MINUS = r'\-'
-t_LPAREN = r'\('
-t_RPAREN = r'\)'
-
-def t_TITLE(t):
- r'title:'
- return t
-
-def t_LINK(t):
- r'link:'
- return t
-
-def t_AUTHOR(t):
- r'author:'
- return t
-
-def t_CONTENT(t):
- r'content:'
- return t
-
-def t_WORD(t):
- r'(\'[^\']+\') | (\"[^\"]+\") | ([^ \n\t\r+\-()\'"]+)'
- t.type = reserved.get(t.value, 'WORD')
- if t.value[0] == '"' and t.value[-1] == '"':
- t.value = t.value[1:-1]
- if t.value[0] == "'" and t.value[-1] == "'":
- t.value = t.value[1:-1]
- return t
-
-t_ignore = ' \n\t\r'
-
-def t_error(t):
- raise Exception
-
-# Parser Rules
-precedence = (
- ('left', 'OR'),
- ('left', 'AND'),
- ('right', 'NOT')
-)
-
-def p_filter(t):
- 'filter : expression'
- t[0] = t[1]
-
-def p_expression_rule(t):
- 'expression : rule'
- t[0] = t[1]
-
-def p_expression_and(t):
- 'expression : expression AND expression'
- t[0] = AndRule(t[1], t[3])
-
-def p_expression_or(t):
- 'expression : expression OR expression'
- t[0] = OrRule(t[1], t[3])
-
-def p_expression_not(t):
- 'expression : NOT expression'
- t[0] = NotRule(t[2])
-
-def p_expression_group(t):
- 'expression : LPAREN expression RPAREN'
- t[0] = t[2]
-
-def p_rule(t):
- 'rule : type qualifier WORD'
- t[0] = Rule(t[1], t[2], t[3])
-
-def p_type(t):
- '''type : PLUS
- | MINUS
- | empty'''
- t[0] = t[1]
-
-def p_qualifier(t):
- '''qualifier : TITLE
- | LINK
- | AUTHOR
- | CONTENT
- | empty'''
- t[0] = t[1]
-
-def p_empty(t):
- 'empty :'
- pass
-
-def p_error(t):
- raise Exception
-
-import ply.lex as lex
-import ply.yacc as yacc
-
-lexer = lex.lex()
-parser = yacc.yacc()
-
-def parse(text):
- return parser.parse(text, lexer=lexer)
-
-if __name__ == '__main__':
- while True:
- text = raw_input('> ')
- print parse(text)
-
\ No newline at end of file
+# Keyword Filter Parser
+
+EXCLUDE = 0
+INCLUDE = 1
+
+ALL = 0xf
+TITLE = 1
+LINK = 2
+AUTHOR = 4
+CONTENT = 8
+
+TYPES = {
+ None: INCLUDE,
+ '+': INCLUDE,
+ '-': EXCLUDE,
+}
+
+QUALIFIERS = {
+ None: ALL,
+ 'title:': TITLE,
+ 'link:': LINK,
+ 'author:': AUTHOR,
+ 'content:': CONTENT,
+}
+
+TYPE_STR = {
+ EXCLUDE: '-',
+ INCLUDE: '+',
+}
+
+QUALIFIER_STR = {
+ ALL: 'all',
+ TITLE: 'title',
+ LINK: 'link',
+ AUTHOR: 'author',
+ CONTENT: 'content',
+}
+
+
+class Rule(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self, type, qualifier, word):
+ """[summary]
+
+ Arguments:
+ type {[type]} -- [description]
+ qualifier {[type]} -- [description]
+ word {[type]} -- [description]
+ """
+
+ self.type = TYPES.get(type, type)
+ self.qualifier = QUALIFIERS.get(qualifier, qualifier)
+ self.word = word
+
+ def evaluate(self, item, ignore_case=True, whole_word=True):
+ """[summary]
+
+ Arguments:
+ item {[type]} -- [description]
+
+ Keyword Arguments:
+ ignore_case {bool} -- [description] (default: {True})
+ whole_word {bool} -- [description] (default: {True})
+
+ Returns:
+ [type] -- [description]
+ """
+
+ strings = []
+ if self.qualifier & TITLE:
+ strings.append(item.title)
+ if self.qualifier & LINK:
+ strings.append(item.link)
+ if self.qualifier & AUTHOR:
+ strings.append(item.author)
+ if self.qualifier & CONTENT:
+ strings.append(item.description)
+ text = '\n'.join(strings)
+ word = self.word
+ if ignore_case:
+ text = text.lower()
+ word = word.lower()
+ if whole_word:
+ text = set(text.split())
+ if word in text:
+ return self.type == INCLUDE
+ else:
+ return self.type == EXCLUDE
+
+ def __str__(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ type = TYPE_STR[self.type]
+ qualifier = QUALIFIER_STR[self.qualifier]
+ return '(%s, %s, "%s")' % (type, qualifier, self.word)
+
+
+class AndRule(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self, left, right):
+ """[summary]
+
+ Arguments:
+ left {[type]} -- [description]
+ right {[type]} -- [description]
+ """
+
+ self.left = left
+ self.right = right
+
+ def evaluate(self, item, ignore_case=True, whole_word=True):
+ """[summary]
+
+ Arguments:
+ item {[type]} -- [description]
+
+ Keyword Arguments:
+ ignore_case {bool} -- [description] (default: {True})
+ whole_word {bool} -- [description] (default: {True})
+
+ Returns:
+ [type] -- [description]
+ """
+
+ a = self.left.evaluate(item, ignore_case, whole_word)
+ b = self.right.evaluate(item, ignore_case, whole_word)
+
+ return a and b
+
+ def __str__(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return '(%s and %s)' % (self.left, self.right)
+
+
+class OrRule(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self, left, right):
+ """[summary]
+
+ Arguments:
+ left {[type]} -- [description]
+ right {[type]} -- [description]
+ """
+
+ self.left = left
+ self.right = right
+
+ def evaluate(self, item, ignore_case=True, whole_word=True):
+ """[summary]
+
+ Arguments:
+ item {[type]} -- [description]
+
+ Keyword Arguments:
+ ignore_case {bool} -- [description] (default: {True})
+ whole_word {bool} -- [description] (default: {True})
+
+ Returns:
+ [type] -- [description]
+ """
+
+ a = self.left.evaluate(item, ignore_case, whole_word)
+ b = self.right.evaluate(item, ignore_case, whole_word)
+
+ return a or b
+
+ def __str__(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return '(%s or %s)' % (self.left, self.right)
+
+
+class NotRule(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self, rule):
+ """[summary]
+
+ Arguments:
+ rule {[type]} -- [description]
+ """
+
+ self.rule = rule
+
+ def evaluate(self, item, ignore_case=True, whole_word=True):
+ """[summary]
+
+ Arguments:
+ item {[type]} -- [description]
+
+ Keyword Arguments:
+ ignore_case {bool} -- [description] (default: {True})
+ whole_word {bool} -- [description] (default: {True})
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return not self.rule.evaluate(item, ignore_case, whole_word)
+
+ def __str__(self):
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return '(not %s)' % (self.rule)
+
+
+# Lexer Rules
+reserved = {
+ 'and': 'AND',
+ 'or': 'OR',
+ 'not': 'NOT',
+}
+
+tokens = [
+ 'PLUS',
+ 'MINUS',
+ 'LPAREN',
+ 'RPAREN',
+ 'TITLE',
+ 'LINK',
+ 'AUTHOR',
+ 'CONTENT',
+ 'WORD',
+] + list(reserved.values())
+
+t_PLUS = r'\+'
+t_MINUS = r'\-'
+t_LPAREN = r'\('
+t_RPAREN = r'\)'
+
+
+def t_TITLE(t):
+ r'title:'
+ return t
+
+
+def t_LINK(t):
+ r'link:'
+ return t
+
+
+def t_AUTHOR(t):
+ r'author:'
+ return t
+
+
+def t_CONTENT(t):
+ r'content:'
+ return t
+
+
+def t_WORD(t):
+ r'(\'[^\']+\') | (\"[^\"]+\") | ([^ \n\t\r+\-()\'"]+)'
+ t.type = reserved.get(t.value, 'WORD')
+ if t.value[0] == '"' and t.value[-1] == '"':
+ t.value = t.value[1:-1]
+ if t.value[0] == "'" and t.value[-1] == "'":
+ t.value = t.value[1:-1]
+ return t
+
+
+t_ignore = ' \n\t\r'
+
+
+def t_error(t):
+ raise Exception
+
+
+# Parser Rules
+precedence = (
+ ('left', 'OR'),
+ ('left', 'AND'),
+ ('right', 'NOT')
+)
+
+
+def p_filter(t):
+ 'filter : expression'
+ t[0] = t[1]
+
+
+def p_expression_rule(t):
+ 'expression : rule'
+ t[0] = t[1]
+
+
+def p_expression_and(t):
+ 'expression : expression AND expression'
+ t[0] = AndRule(t[1], t[3])
+
+
+def p_expression_or(t):
+ 'expression : expression OR expression'
+ t[0] = OrRule(t[1], t[3])
+
+
+def p_expression_not(t):
+ 'expression : NOT expression'
+ t[0] = NotRule(t[2])
+
+
+def p_expression_group(t):
+ 'expression : LPAREN expression RPAREN'
+ t[0] = t[2]
+
+
+def p_rule(t):
+ 'rule : type qualifier WORD'
+ t[0] = Rule(t[1], t[2], t[3])
+
+
+def p_type(t):
+ '''type : PLUS
+ | MINUS
+ | empty'''
+ t[0] = t[1]
+
+
+def p_qualifier(t):
+ '''qualifier : TITLE
+ | LINK
+ | AUTHOR
+ | CONTENT
+ | empty'''
+ t[0] = t[1]
+
+
+def p_empty(t):
+ 'empty :'
+ pass
+
+
+def p_error(t):
+ raise Exception
+
+
+try:
+ import ply.lex as lex
+ import ply.yacc as yacc
+except ModuleNotFoundError:
+ print("\n\tpip install ply\n")
+
+lexer = lex.lex()
+parser = yacc.yacc()
+
+
+def parse(text):
+ """[summary]
+
+ Arguments:
+ text {[type]} -- [description]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ return parser.parse(text, lexer=lexer)
+
+
+if __name__ == '__main__':
+ while True:
+ text = eval(input('> '))
+ print((parse(text)))
+
+# EOF
diff --git a/idle.py b/idle.py
index cc13e9e..da291c1 100644
--- a/idle.py
+++ b/idle.py
@@ -1,30 +1,79 @@
-import sys
-
-if sys.platform == 'win32':
- from ctypes import *
-
- class LASTINPUTINFO(Structure):
- _fields_ = [
- ('cbSize', c_uint),
- ('dwTime', c_int),
- ]
-
- def get_idle_duration():
- lastInputInfo = LASTINPUTINFO()
- lastInputInfo.cbSize = sizeof(lastInputInfo)
- if windll.user32.GetLastInputInfo(byref(lastInputInfo)):
- millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime
- return millis / 1000.0
- else:
- return 0
-else:
- def get_idle_duration():
- return 0
-
-if __name__ == '__main__':
- import time
- while True:
- duration = get_idle_duration()
- print 'User idle for %.2f seconds.' % duration
- time.sleep(1)
-
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+"""[summary]
+
+Returns:
+ [type] -- [description]
+"""
+
+import logging
+import sys
+import time
+
+if sys.platform.startswith('win32'):
+ from ctypes import *
+
+if sys.platform.startswith('win32'):
+
+ # Detecting idle time using python
+ # https://stackoverflow.com/questions/911856/detecting-idle-time-using-python
+ #
+ # https://stackoverflow.com/questions/217157/how-can-i-determine-the-display-idle-time-from-python-in-windows-linux-and-mac?noredirect=1&lq=1
+ # TODO:
+
+ class LASTINPUTINFO(Structure):
+ """[summary]
+
+ Arguments:
+ Structure {[type]} -- [description]
+ """
+
+ _fields_ = [
+ ('cbSize', c_uint),
+ ('dwTime', c_int),
+ ]
+
+ def get_idle_duration():
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ lastInputInfo = LASTINPUTINFO()
+ lastInputInfo.cbSize = sizeof(lastInputInfo)
+
+ if windll.user32.GetLastInputInfo(byref(lastInputInfo)):
+ millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime
+ return millis / 1000.0
+ else:
+ return 0
+
+elif sys.platform.startswith('darwin'):
+
+ print('We are in MacOX') # FIXME: ¿?
+
+ def get_idle_duration():
+ return 0
+
+elif sys.platform.startswith('linux'):
+
+ print('We are in linux') # FIXME: ¿?
+
+ def get_idle_duration():
+ return 0
+
+else:
+ pass
+
+ # def get_idle_duration():
+ # return 0
+
+if __name__ == '__main__':
+
+ while True:
+ duration = get_idle_duration()
+ logging.debug(('User idle for %.2f seconds.' % duration))
+ time.sleep(1)
+
+# EOF
diff --git a/ipc.py b/ipc.py
index b92a51f..7bbe195 100644
--- a/ipc.py
+++ b/ipc.py
@@ -1,102 +1,336 @@
-import wx
-import sys
-import util
-
-class CallbackContainer(object):
- def __init__(self):
- self.callback = None
- def __call__(self, message):
- if self.callback:
- wx.CallAfter(self.callback, message)
-
-if sys.platform == 'win32':
- import win32file
- import win32pipe
- import time
-
- def init():
- container = CallbackContainer()
- message = '\n'.join(sys.argv[1:])
- name = r'\\.\pipe\FeedNotifier_%s' % wx.GetUserId()
- if client(name, message):
- return None, message
- else:
- util.start_thread(server, name, container)
- return container, message
-
- def server(name, callback_func):
- buffer = 4096
- timeout = 1000
- error = False
- while True:
- if error:
- time.sleep(1)
- error = False
- handle = win32pipe.CreateNamedPipe(
- name,
- win32pipe.PIPE_ACCESS_INBOUND,
- win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT,
- win32pipe.PIPE_UNLIMITED_INSTANCES,
- buffer,
- buffer,
- timeout,
- None)
- if handle == win32file.INVALID_HANDLE_VALUE:
- error = True
- continue
- try:
- if win32pipe.ConnectNamedPipe(handle) != 0:
- error = True
- else:
- code, message = win32file.ReadFile(handle, buffer, None)
- if code == 0:
- callback_func(message)
- else:
- error = True
- except Exception:
- error = True
- finally:
- win32pipe.DisconnectNamedPipe(handle)
- win32file.CloseHandle(handle)
-
- def client(name, message):
- try:
- file = open(name, 'wb')
- file.write(message)
- file.close()
- return True
- except IOError:
- return False
-else:
- import functools
- import socket
- import SocketServer
-
- def init():
- container = CallbackContainer()
- message = '\n'.join(sys.argv[1:])
- host, port = 'localhost', 31763
- try:
- server(host, port, container)
- return container, message
- except socket.error:
- client(host, port, message)
- return None, message
-
- def server(host, port, callback_func):
- class Handler(SocketServer.StreamRequestHandler):
- def __init__(self, callback_func, *args, **kwargs):
- self.callback_func = callback_func
- SocketServer.StreamRequestHandler.__init__(self, *args, **kwargs)
- def handle(self):
- data = self.rfile.readline().strip()
- self.callback_func(data)
- server = SocketServer.TCPServer((host, port), functools.partial(Handler, callback_func))
- util.start_thread(server.serve_forever)
-
- def client(host, port, message):
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.connect((host, port))
- sock.send(message)
- sock.close()
-
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+"""[summary]
+
+Returns:
+ [type] -- [description]
+"""
+
+import functools
+import logging
+import socket
+import socketserver
+import sys
+import time
+
+import wx
+
+import util
+
+if sys.platform.startswith('win32'):
+ try:
+ import win32file
+ import win32pipe
+ except ModuleNotFoundError:
+ sys.exit("\n\tpip install pywin32 ...\n")
+
+
+class CallbackContainer(object):
+ """[summary]
+
+ Arguments:
+ object {[type]} -- [description]
+ """
+
+ def __init__(self):
+ """Initialize CallbackContainer class.
+ """
+
+ self.callback = None
+ logging.debug("Return callback=%s", self.callback)
+
+ def __call__(self, message):
+ """Making GUI method calls from non-GUI threads.
+
+ Any extra positional or keyword args are passed on to the callable
+ when it is called.
+
+ Arguments:
+ message {[type]} -- [description]
+ """
+
+ if self.callback:
+ wx.CallAfter(self.callback, message)
+
+ logging.debug("Launch wx.CallAfter(self.callback, message)")
+
+
+if sys.platform.startswith('win32'):
+
+ def init():
+ """initialize the thread server
+
+ Returns:
+ [type] -- [description]
+ """
+
+ logging.debug('initializing')
+
+ container = CallbackContainer()
+ message = '\n'.join(sys.argv[1:])
+ name = r'\\.\pipe\FeedNotifier_%s' % wx.GetUserId()
+
+ if client(name, message):
+ logging.debug("Initialized: return message='%s' and name='%s'", message, name)
+ return None, message
+ else:
+ logging.debug("Initialized: message='%s' and name='%s'", message, name)
+
+ logging.debug('Jump to util::start_thread()')
+ util.start_thread(server, name, container)
+ logging.debug('return of util::start_thread()')
+
+ return container, message
+
+ def server(name, callback_func):
+ """[summary]
+
+ Arguments:
+ name {[type]} -- [description]
+ callback_func {[type]} -- [description]
+ """
+
+ buffer = 4096
+ timeout = 1000
+ error = False
+
+ while True:
+ if error:
+ time.sleep(1)
+ error = False
+
+ handle = win32pipe.CreateNamedPipe(
+ name,
+ win32pipe.PIPE_ACCESS_INBOUND,
+ win32pipe.PIPE_TYPE_BYTE |
+ win32pipe.PIPE_READMODE_BYTE |
+ win32pipe.PIPE_WAIT,
+ win32pipe.PIPE_UNLIMITED_INSTANCES,
+ buffer,
+ buffer,
+ timeout,
+ None)
+
+ if handle == win32file.INVALID_HANDLE_VALUE:
+ error = True
+ continue
+
+ try:
+ if win32pipe.ConnectNamedPipe(handle) != 0:
+ error = True
+ else:
+ code, message = win32file.ReadFile(handle, buffer, None)
+ if code == 0:
+ callback_func(message)
+ else:
+ error = True
+ except Exception:
+ error = True
+ finally:
+ win32pipe.DisconnectNamedPipe(handle)
+ win32file.CloseHandle(handle)
+
+ def client(name, message):
+ """[summary]
+
+ Arguments:
+ name {[type]} -- [description]
+ message {[type]} -- [description]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ try:
+ file = open(name, 'wb')
+ file.write(message)
+ file.close()
+ return True
+ except IOError:
+ return False
+
+elif sys.platform.startswith('darwin'):
+
+ sys.exit('\n\tdarwin platform not soported\n')
+
+elif sys.platform.startswith('linux'):
+
+ # sys.exit('\n\tlinux in not suported at time...\n')
+
+ def init():
+ """initialize the thread server
+
+ Returns:
+ [type] -- [description]
+ """
+
+ logging.debug(f'Initializing ipc')
+
+ container = CallbackContainer()
+ # print(f"Container: {container}") # FIXME: delete this
+ # print(f"Container: {type(container)}") # FIXME: delete this
+ # print(f"Container: {container.__doc__}") # FIXME: delete this
+ message = '\n'.join(sys.argv[1:])
+ name = r'\\.\pipe\FeedNotifier_%s' % wx.GetUserId()
+
+ # if client(name, message):
+ # print('ipc::init:win32 - Existen "message" y "name"')
+ # print('Salimos de ipc::init()::linux')
+ # return None, message
+ # else:
+ # print('ipc::init:win32 - No existen "message" y "name"')
+ # print('Salimos de ipc::init()::linux')
+ # # jump to util.py
+ # util.start_thread(server, name, container)
+ #
+ # return container, message
+
+ logging.debug(f'Initialized ipc')
+
+ def server(name, callback_func):
+ """[summary]
+
+ Arguments:
+ name {[type]} -- [description]
+ callback_func {[type]} -- [description]
+ """
+
+ buffer = 4096
+ timeout = 1000
+ error = False
+
+ while True:
+ if error:
+ time.sleep(1)
+ error = False
+
+ handle = win32pipe.CreateNamedPipe(
+ name,
+ win32pipe.PIPE_ACCESS_INBOUND,
+ win32pipe.PIPE_TYPE_BYTE |
+ win32pipe.PIPE_READMODE_BYTE |
+ win32pipe.PIPE_WAIT,
+ win32pipe.PIPE_UNLIMITED_INSTANCES,
+ buffer,
+ buffer,
+ timeout,
+ None)
+
+ if handle == win32file.INVALID_HANDLE_VALUE:
+ error = True
+ continue
+
+ try:
+ if win32pipe.ConnectNamedPipe(handle) != 0:
+ error = True
+ else:
+ code, message = win32file.ReadFile(handle, buffer, None)
+ if code == 0:
+ callback_func(message)
+ else:
+ error = True
+ except Exception:
+ error = True
+ finally:
+ win32pipe.DisconnectNamedPipe(handle)
+ win32file.CloseHandle(handle)
+
+ def client(name, message):
+ """[summary]
+
+ Arguments:
+ name {[type]} -- [description]
+ message {[type]} -- [description]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ try:
+ file = open(name, 'wb')
+ file.write(message)
+ file.close()
+ return True
+ except IOError:
+ return False
+
+
+else:
+
+ # import functools # FIXME: delete this
+ # import socket # FIXME: delete this
+ # import socketserver # FIXME: delete this
+
+ def init():
+ """[summary]
+
+ Returns:
+ [type] -- [description]
+ """
+
+ container = CallbackContainer()
+ message = '\n'.join(sys.argv[1:])
+ host, port = 'localhost', 31763
+
+ try:
+ server(host, port, container)
+ return container, message
+ except socket.error:
+ client(host, port, message)
+ return None, message
+
+ def server(host, port, callback_func):
+ """[summary]
+
+ Arguments:
+ host {[type]} -- [description]
+ port {[type]} -- [description]
+ callback_func {[type]} -- [description]
+ """
+
+ class Handler(socketserver.StreamRequestHandler):
+ """[summary]
+
+ Arguments:
+ socketserver {[type]} -- [description]
+ """
+
+ def __init__(self, callback_func, *args, **kwargs):
+ """[summary]
+
+ Arguments:
+ callback_func {[type]} -- [description]
+ """
+
+ self.callback_func = callback_func
+ socketserver.StreamRequestHandler.__init__(self,
+ *args,
+ **kwargs)
+
+ def handle(self):
+ """[summary]
+ """
+
+ data = self.rfile.readline().strip()
+ self.callback_func(data)
+
+ server = socketserver.TCPServer((host, port),
+ functools.partial(Handler,
+ callback_func))
+ util.start_thread(server.serve_forever)
+
+ def client(host, port, message):
+ """[summary]
+
+ Arguments:
+ host {[type]} -- [description]
+ port {[type]} -- [description]
+ message {[type]} -- [description]
+ """
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((host, port))
+ sock.send(message)
+ sock.close()
+
+# EOF
diff --git a/main.py b/main.py
index e2f2d7c..5a934e4 100644
--- a/main.py
+++ b/main.py
@@ -1,48 +1,138 @@
-def init_path():
- import os
- import dummy
- file = dummy.__file__
- file = os.path.abspath(file)
- while file and not os.path.isdir(file):
- file, ext = os.path.split(file)
- os.chdir(file)
-
-def init_logging():
- import sys
- import logging
- logging.basicConfig(
- level=logging.DEBUG,
- filename='log.txt',
- filemode='w',
- format='%(asctime)s %(levelname)s %(message)s',
- datefmt='%H:%M:%S',
- )
- if not hasattr(sys, 'frozen'):
- console = logging.StreamHandler(sys.stdout)
- console.setLevel(logging.DEBUG)
- formatter = logging.Formatter(
- '%(asctime)s %(levelname)s %(message)s',
- '%H:%M:%S',
- )
- console.setFormatter(formatter)
- logging.getLogger('').addHandler(console)
-
-def main():
- init_path()
- init_logging()
- import wx
- import ipc
- import controller
- container, message = ipc.init()
- if not container:
- return
- app = wx.PySimpleApp()#redirect=True, filename='log.txt')
- wx.Log_SetActiveTarget(wx.LogStderr())
- ctrl = controller.Controller()
- container.callback = ctrl.parse_args
- container(message)
- app.MainLoop()
-
-if __name__ == '__main__':
- main()
-
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+"""
+docstring
+"""
+
+# import pdb; pdb.set_trace()
+# import gettext
+import logging
+import os
+import sys
+
+import controller
+import dummy
+import ipc
+
+try:
+ import wx
+ # import wx.Locale
+ # import wx.GetTranslation
+except ImportError:
+ sys.exit('\n\tInstall wxPython.\n')
+
+
+APPNAME = os.path.splitext(os.path.basename(sys.argv[0]))[0]
+INI_FILENAME = APPNAME + '.ini'
+LOG_FILENAME = APPNAME + '.log'
+LOG_LEVEL = logging.DEBUG #: Example: "DEBUG" o "WARNING"
+
+# languagelist = [locale.getdefaultlocale()[0], 'en_US']
+# t = gettext.translation('FeedNotifier', localedir, ['es_ES', 'en_US'])
+# _ = t.ugettext
+# # pygettext -d FeedNotifier main.py
+
+
+def init_path():
+ """Set the current directory.
+ """
+
+ file = dummy.__file__
+ file = os.path.abspath(file)
+
+ while file and not os.path.isdir(file):
+ file = os.path.split(file)[0]
+ os.chdir(file)
+
+
+def init_logging():
+ """[summary]
+ """
+
+ # import sys # FIXME: delete this import
+ # import logging
+
+ mformat = "%(asctime)s" \
+ " %(levelname)s %(module)s:%(lineno)s %(funcName)s %(message)s"
+
+ logging.basicConfig(format=mformat,
+ datefmt='%Y%m%d%H%M%S',
+ filename=LOG_FILENAME,
+ level=LOG_LEVEL,
+ # style="{"
+ )
+
+ logging.logThreads = 0
+ logging.logProcesses = 0
+
+ # logging.basicConfig(
+ # level=LOG_LEVEL,
+ # filename=LOG_FILENAME,
+ # filemode='w',
+ # format='%(asctime)s %(levelname)s %(message)s',
+ # datefmt='%H:%M:%S',
+ # )
+
+ # if not hasattr(sys, 'frozen'):
+ # console = logging.StreamHandler(sys.stdout)
+ # console.setLevel(logging.DEBUG)
+ # formatter = logging.Formatter(
+ # '%(asctime)s %(levelname)s %(message)s',
+ # '%H:%M:%S',
+ # )
+ # console.setFormatter(formatter)
+ # logging.getLogger('').addHandler(console)
+
+ # logging.debug('Test DEBUG') # DEBUG 10
+ # logging.info('Test INFO') # INFO 20
+ # logging.warning('Test WARNING') # WARNING 30
+ # logging.error('Test ERROR') # ERROR 40
+ # logging.critical('Test CRITICAL') # CRITICAL 50
+
+
+def main():
+ """Start point of app.
+ """
+
+ init_path()
+ init_logging()
+
+ # import sys # FIXME: delete this import
+ # import wx
+ # import ipc
+ # import controller
+
+ logging.debug('-> ipc.init() - In') # FIXME: delete this
+ try:
+ container, message = ipc.init()
+ except TypeError:
+ logging.error('IPC no initialize!\nExit.')
+ sys.exit('Problems! IPC no initialize')
+ logging.debug(f'container: {container}')
+ logging.debug(f'message: {message}')
+ logging.debug('<- ipc.init() - Out')
+
+ if not container:
+ logging.error('main:: The container could not be created.')
+ return
+
+ logging.debug(f'Initializing wx.app class')
+
+ # app = wx.App() # redirect=True, filename='log.txt')
+ app = wx.App(redirect=True, filename="log.txt", useBestVisual=True)
+ logging.debug(f'Initialized wx.app class')
+ wx.Log.SetActiveTarget(wx.LogStderr())
+ logging.debug(f'Initializing wx.Log.SetActiveTarget(wx.LogStderr())')
+ ctrl = controller.Controller()
+ logging.debug(f'Initializing ctrl with controller.Controller()')
+ container.callback = ctrl.parse_args
+ logging.debug(f'Initializing container.callback with ctrl.parse_args')
+ container(message)
+ logging.debug(f'Initializing container(message)')
+ app.MainLoop()
+
+
+if __name__ == '__main__':
+ main()
+
+# EOF
diff --git a/parsetab.py b/parsetab.py
index 9539607..4cef381 100644
--- a/parsetab.py
+++ b/parsetab.py
@@ -1,45 +1,56 @@
-
-# parsetab.py
-# This file is automatically generated. Do not edit.
-_tabversion = '3.2'
-
-_lr_method = 'LALR'
-
-_lr_signature = '\x03\xd8\xc9Q1\x0e\x13W\xf5\xf7\xacu\x8b$z\xd4'
-
-_lr_action_items = {'AND':([2,8,16,17,20,21,22,23,],[-2,18,18,-5,-7,-6,-3,18,]),'WORD':([0,1,3,5,6,7,9,10,11,12,13,14,15,18,19,],[-16,-16,-9,-8,-16,-16,-10,20,-13,-11,-14,-12,-15,-16,-16,]),'AUTHOR':([0,1,3,5,6,7,9,18,19,],[-16,11,-9,-8,-16,-16,-10,-16,-16,]),'TITLE':([0,1,3,5,6,7,9,18,19,],[-16,12,-9,-8,-16,-16,-10,-16,-16,]),'OR':([2,8,16,17,20,21,22,23,],[-2,19,19,-5,-7,-6,-3,-4,]),'CONTENT':([0,1,3,5,6,7,9,18,19,],[-16,13,-9,-8,-16,-16,-10,-16,-16,]),'LINK':([0,1,3,5,6,7,9,18,19,],[-16,14,-9,-8,-16,-16,-10,-16,-16,]),'LPAREN':([0,6,7,18,19,],[6,6,6,6,6,]),'NOT':([0,6,7,18,19,],[7,7,7,7,7,]),'PLUS':([0,6,7,18,19,],[5,5,5,5,5,]),'$end':([2,4,8,17,20,21,22,23,],[-2,0,-1,-5,-7,-6,-3,-4,]),'MINUS':([0,6,7,18,19,],[3,3,3,3,3,]),'RPAREN':([2,16,17,20,21,22,23,],[-2,21,-5,-7,-6,-3,-4,]),}
-
-_lr_action = { }
-for _k, _v in _lr_action_items.items():
- for _x,_y in zip(_v[0],_v[1]):
- if not _x in _lr_action: _lr_action[_x] = { }
- _lr_action[_x][_k] = _y
-del _lr_action_items
-
-_lr_goto_items = {'qualifier':([1,],[10,]),'type':([0,6,7,18,19,],[1,1,1,1,1,]),'rule':([0,6,7,18,19,],[2,2,2,2,2,]),'filter':([0,],[4,]),'expression':([0,6,7,18,19,],[8,16,17,22,23,]),'empty':([0,1,6,7,18,19,],[9,15,9,9,9,9,]),}
-
-_lr_goto = { }
-for _k, _v in _lr_goto_items.items():
- for _x,_y in zip(_v[0],_v[1]):
- if not _x in _lr_goto: _lr_goto[_x] = { }
- _lr_goto[_x][_k] = _y
-del _lr_goto_items
-_lr_productions = [
- ("S' -> filter","S'",1,None,None,None),
- ('filter -> expression','filter',1,'p_filter','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',161),
- ('expression -> rule','expression',1,'p_expression_rule','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',165),
- ('expression -> expression AND expression','expression',3,'p_expression_and','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',169),
- ('expression -> expression OR expression','expression',3,'p_expression_or','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',173),
- ('expression -> NOT expression','expression',2,'p_expression_not','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',177),
- ('expression -> LPAREN expression RPAREN','expression',3,'p_expression_group','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',181),
- ('rule -> type qualifier WORD','rule',3,'p_rule','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',185),
- ('type -> PLUS','type',1,'p_type','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',189),
- ('type -> MINUS','type',1,'p_type','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',190),
- ('type -> empty','type',1,'p_type','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',191),
- ('qualifier -> TITLE','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',195),
- ('qualifier -> LINK','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',196),
- ('qualifier -> AUTHOR','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',197),
- ('qualifier -> CONTENT','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',198),
- ('qualifier -> empty','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',199),
- ('empty -> ','empty',0,'p_empty','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',203),
-]
+# -*- coding: utf-8 -*-
+
+# parsetab.py
+# This file is automatically generated. Do not edit.
+# pylint: disable=W,C,R
+_tabversion = '3.10'
+
+_lr_method = 'LALR'
+
+_lr_signature = 'leftORleftANDrightNOTAND AUTHOR CONTENT LINK LPAREN MINUS NOT OR PLUS RPAREN TITLE WORDfilter : expressionexpression : ruleexpression : expression AND expressionexpression : expression OR expressionexpression : NOT expressionexpression : LPAREN expression RPARENrule : type qualifier WORDtype : PLUS\n | MINUS\n | emptyqualifier : TITLE \n | LINK \n | AUTHOR \n | CONTENT\n | emptyempty :'
+
+_lr_action_items = {'NOT': ([0, 4, 5, 10, 11, ], [4, 4, 4, 4, 4, ]), 'LPAREN': ([0, 4, 5, 10, 11, ], [5, 5, 5, 5, 5, ]), 'PLUS': ([0, 4, 5, 10, 11, ], [7, 7, 7, 7, 7, ]), 'MINUS': ([0, 4, 5, 10, 11, ], [8, 8, 8, 8, 8, ]), 'TITLE': ([0, 4, 5, 6, 7, 8, 9, 10, 11, ], [-16, -16, -16, 15, -8, -9, -10, -16, -16, ]), 'LINK': ([0, 4, 5, 6, 7, 8, 9, 10, 11, ], [-16, -16, -16, 16, -8, -9, -10, -16, -16, ]), 'AUTHOR': ([0, 4, 5, 6, 7, 8, 9, 10, 11, ], [-16, -16, -16, 17, -8, -9, -10, -16, -16, ]), 'CONTENT': ([0, 4, 5, 6, 7,
+ 8, 9, 10, 11, ], [-16, -16, -16, 18, -8, -9, -10, -16, -16, ]), 'WORD': ([0, 4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, ], [-16, -16, -16, -16, -8, -9, -10, -16, -16, 23, -11, -12, -13, -14, -15, ]), '$end': ([1, 2, 3, 12, 20, 21, 22, 23, ], [0, -1, -2, -5, -3, -4, -6, -7, ]), 'AND': ([2, 3, 12, 13, 20, 21, 22, 23, ], [10, -2, -5, 10, -3, 10, -6, -7, ]), 'OR': ([2, 3, 12, 13, 20, 21, 22, 23, ], [11, -2, -5, 11, -3, -4, -6, -7, ]), 'RPAREN': ([3, 12, 13, 20, 21, 22, 23, ], [-2, -5, 22, -3, -4, -6, -7, ]), }
+
+_lr_action = {}
+for _k, _v in _lr_action_items.items():
+ for _x, _y in zip(_v[0], _v[1]):
+ if not _x in _lr_action:
+ _lr_action[_x] = {}
+ _lr_action[_x][_k] = _y
+del _lr_action_items
+
+_lr_goto_items = {'filter': ([0, ], [1, ]), 'expression': ([0, 4, 5, 10, 11, ], [2, 12, 13, 20, 21, ]), 'rule': ([0, 4, 5, 10, 11, ], [3, 3, 3, 3, 3, ]), 'type': (
+ [0, 4, 5, 10, 11, ], [6, 6, 6, 6, 6, ]), 'empty': ([0, 4, 5, 6, 10, 11, ], [9, 9, 9, 19, 9, 9, ]), 'qualifier': ([6, ], [14, ]), }
+
+_lr_goto = {}
+for _k, _v in _lr_goto_items.items():
+ for _x, _y in zip(_v[0], _v[1]):
+ if not _x in _lr_goto:
+ _lr_goto[_x] = {}
+ _lr_goto[_x][_k] = _y
+del _lr_goto_items
+_lr_productions = [
+ ("S' -> filter", "S'", 1, None, None, None),
+ ('filter -> expression', 'filter', 1, 'p_filter', 'filters.py', 162),
+ ('expression -> rule', 'expression', 1,
+ 'p_expression_rule', 'filters.py', 166),
+ ('expression -> expression AND expression',
+ 'expression', 3, 'p_expression_and', 'filters.py', 170),
+ ('expression -> expression OR expression',
+ 'expression', 3, 'p_expression_or', 'filters.py', 174),
+ ('expression -> NOT expression', 'expression',
+ 2, 'p_expression_not', 'filters.py', 178),
+ ('expression -> LPAREN expression RPAREN', 'expression',
+ 3, 'p_expression_group', 'filters.py', 182),
+ ('rule -> type qualifier WORD', 'rule', 3, 'p_rule', 'filters.py', 186),
+ ('type -> PLUS', 'type', 1, 'p_type', 'filters.py', 190),
+ ('type -> MINUS', 'type', 1, 'p_type', 'filters.py', 191),
+ ('type -> empty', 'type', 1, 'p_type', 'filters.py', 192),
+ ('qualifier -> TITLE', 'qualifier', 1, 'p_qualifier', 'filters.py', 196),
+ ('qualifier -> LINK', 'qualifier', 1, 'p_qualifier', 'filters.py', 197),
+ ('qualifier -> AUTHOR', 'qualifier', 1, 'p_qualifier', 'filters.py', 198),
+ ('qualifier -> CONTENT', 'qualifier', 1, 'p_qualifier', 'filters.py', 199),
+ ('qualifier -> empty', 'qualifier', 1, 'p_qualifier', 'filters.py', 200),
+ ('empty -> ', 'empty', 0, 'p_empty', 'filters.py', 204),
+]
diff --git a/patch/build_exe.py b/patch/build_exe.py
index cd3138d..3e997fa 100644
--- a/patch/build_exe.py
+++ b/patch/build_exe.py
@@ -1,1712 +1,1765 @@
-# Changes:
-#
-# can now specify 'zipfile = None', in this case the Python module
-# library archive is appended to the exe.
-
-# Todo:
-#
-# Make 'unbuffered' a per-target option
-
-from distutils.core import Command
-from distutils.spawn import spawn
-from distutils.errors import *
-import sys, os, imp, types, stat
-import marshal
-import zipfile
-import sets
-import tempfile
-import struct
-import re
-
-is_win64 = struct.calcsize("P") == 8
-
-def _is_debug_build():
- for ext, _, _ in imp.get_suffixes():
- if ext == "_d.pyd":
- return True
- return False
-
-is_debug_build = _is_debug_build()
-
-if is_debug_build:
- python_dll = "python%d%d_d.dll" % sys.version_info[:2]
-else:
- python_dll = "python%d%d.dll" % sys.version_info[:2]
-
-# resource constants
-RT_BITMAP=2
-RT_MANIFEST=24
-
-# Pattern for modifying the 'requestedExecutionLevel' in the manifest. Groups
-# are setup so all text *except* for the values is matched.
-pat_manifest_uac = re.compile(r'(^.*", path
- mod = imp.load_dynamic(__name__, path)
-## mod.frozen = 1
-__load()
-del __load
-"""
-
-# A very loosely defined "target". We assume either a "script" or "modules"
-# attribute. Some attributes will be target specific.
-class Target:
- # A custom requestedExecutionLevel for the User Access Control portion
- # of the manifest for the target. May be a string, which will be used for
- # the 'requestedExecutionLevel' portion and False for 'uiAccess', or a tuple
- # of (string, bool) which specifies both values. If specified and the
- # target's 'template' executable has no manifest (ie, python 2.5 and
- # earlier), then a default manifest is created, otherwise the manifest from
- # the template is copied then updated.
- uac_info = None
-
- def __init__(self, **kw):
- self.__dict__.update(kw)
- # If modules is a simple string, assume they meant list
- m = self.__dict__.get("modules")
- if m and type(m) in types.StringTypes:
- self.modules = [m]
- def get_dest_base(self):
- dest_base = getattr(self, "dest_base", None)
- if dest_base: return dest_base
- script = getattr(self, "script", None)
- if script:
- return os.path.basename(os.path.splitext(script)[0])
- modules = getattr(self, "modules", None)
- assert modules, "no script, modules or dest_base specified"
- return modules[0].split(".")[-1]
-
- def validate(self):
- resources = getattr(self, "bitmap_resources", []) + \
- getattr(self, "icon_resources", [])
- for r_id, r_filename in resources:
- if type(r_id) != type(0):
- raise DistutilsOptionError, "Resource ID must be an integer"
- if not os.path.isfile(r_filename):
- raise DistutilsOptionError, "Resource filename '%s' does not exist" % r_filename
-
-def FixupTargets(targets, default_attribute):
- if not targets:
- return targets
- ret = []
- for target_def in targets:
- if type(target_def) in types.StringTypes :
- # Create a default target object, with the string as the attribute
- target = Target(**{default_attribute: target_def})
- else:
- d = getattr(target_def, "__dict__", target_def)
- if not d.has_key(default_attribute):
- raise DistutilsOptionError, \
- "This target class requires an attribute '%s'" % default_attribute
- target = Target(**d)
- target.validate()
- ret.append(target)
- return ret
-
-class py2exe(Command):
- description = ""
- # List of option tuples: long name, short name (None if no short
- # name), and help string.
- user_options = [
- ('optimize=', 'O',
- "optimization level: -O1 for \"python -O\", "
- "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
- ('dist-dir=', 'd',
- "directory to put final built distributions in (default is dist)"),
-
- ("excludes=", 'e',
- "comma-separated list of modules to exclude"),
- ("dll-excludes=", None,
- "comma-separated list of DLLs to exclude"),
- ("ignores=", None,
- "comma-separated list of modules to ignore if they are not found"),
- ("includes=", 'i',
- "comma-separated list of modules to include"),
- ("packages=", 'p',
- "comma-separated list of packages to include"),
- ("skip-scan=", None,
- "comma-separated list of modules not to scan for imported modules"),
-
- ("compressed", 'c',
- "create a compressed zipfile"),
-
- ("xref", 'x',
- "create and show a module cross reference"),
-
- ("bundle-files=", 'b',
- "bundle dlls in the zipfile or the exe. Valid levels are 1, 2, or 3 (default)"),
-
- ("skip-archive", None,
- "do not place Python bytecode files in an archive, put them directly in the file system"),
-
- ("ascii", 'a',
- "do not automatically include encodings and codecs"),
-
- ('custom-boot-script=', None,
- "Python file that will be run when setting up the runtime environment"),
- ]
-
- boolean_options = ["compressed", "xref", "ascii", "skip-archive"]
-
- def initialize_options (self):
- self.xref =0
- self.compressed = 0
- self.unbuffered = 0
- self.optimize = 0
- self.includes = None
- self.excludes = None
- self.skip_scan = None
- self.ignores = None
- self.packages = None
- self.dist_dir = None
- self.dll_excludes = None
- self.typelibs = None
- self.bundle_files = 3
- self.skip_archive = 0
- self.ascii = 0
- self.custom_boot_script = None
-
- def finalize_options (self):
- self.optimize = int(self.optimize)
- self.excludes = fancy_split(self.excludes)
- self.includes = fancy_split(self.includes)
- self.skip_scan = fancy_split(self.skip_scan)
- self.ignores = fancy_split(self.ignores)
- self.bundle_files = int(self.bundle_files)
- if self.bundle_files < 1 or self.bundle_files > 3:
- raise DistutilsOptionError, \
- "bundle-files must be 1, 2, or 3, not %s" % self.bundle_files
- if is_win64 and self.bundle_files < 3:
- raise DistutilsOptionError, \
- "bundle-files %d not yet supported on win64" % self.bundle_files
- if self.skip_archive:
- if self.compressed:
- raise DistutilsOptionError, \
- "can't compress when skipping archive"
- if self.distribution.zipfile is None:
- raise DistutilsOptionError, \
- "zipfile cannot be None when skipping archive"
- # includes is stronger than excludes
- for m in self.includes:
- if m in self.excludes:
- self.excludes.remove(m)
- self.packages = fancy_split(self.packages)
- self.set_undefined_options('bdist',
- ('dist_dir', 'dist_dir'))
- self.dll_excludes = [x.lower() for x in fancy_split(self.dll_excludes)]
-
- def run(self):
- build = self.reinitialize_command('build')
- build.run()
- sys_old_path = sys.path[:]
- if build.build_platlib is not None:
- sys.path.insert(0, build.build_platlib)
- if build.build_lib is not None:
- sys.path.insert(0, build.build_lib)
- try:
- self._run()
- finally:
- sys.path = sys_old_path
-
- def _run(self):
- self.create_directories()
- self.plat_prepare()
- self.fixup_distribution()
-
- dist = self.distribution
-
- # all of these contain module names
- required_modules = []
- for target in dist.com_server + dist.service + dist.ctypes_com_server:
- required_modules.extend(target.modules)
- # and these contains file names
- required_files = [target.script
- for target in dist.windows + dist.console]
-
- mf = self.create_modulefinder()
-
- # These are the name of a script, but used as a module!
- for f in dist.isapi:
- mf.load_file(f.script)
-
- if self.typelibs:
- print "*** generate typelib stubs ***"
- from distutils.dir_util import mkpath
- genpy_temp = os.path.join(self.temp_dir, "win32com", "gen_py")
- mkpath(genpy_temp)
- num_stubs = collect_win32com_genpy(genpy_temp,
- self.typelibs,
- verbose=self.verbose,
- dry_run=self.dry_run)
- print "collected %d stubs from %d type libraries" \
- % (num_stubs, len(self.typelibs))
- mf.load_package("win32com.gen_py", genpy_temp)
- self.packages.append("win32com.gen_py")
-
- # monkey patching the compile builtin.
- # The idea is to include the filename in the error message
- orig_compile = compile
- import __builtin__
- def my_compile(source, filename, *args):
- try:
- result = orig_compile(source, filename, *args)
- except Exception, details:
- raise DistutilsError, "compiling '%s' failed\n %s: %s" % \
- (filename, details.__class__.__name__, details)
- return result
- __builtin__.compile = my_compile
-
- print "*** searching for required modules ***"
- self.find_needed_modules(mf, required_files, required_modules)
-
- print "*** parsing results ***"
- py_files, extensions, builtins = self.parse_mf_results(mf)
-
- if self.xref:
- mf.create_xref()
-
- print "*** finding dlls needed ***"
- dlls = self.find_dlls(extensions)
- self.plat_finalize(mf.modules, py_files, extensions, dlls)
- dlls = [item for item in dlls
- if os.path.basename(item).lower() not in self.dll_excludes]
- # should we filter self.other_depends in the same way?
-
- print "*** create binaries ***"
- self.create_binaries(py_files, extensions, dlls)
-
- self.fix_badmodules(mf)
-
- if mf.any_missing():
- print "The following modules appear to be missing"
- print mf.any_missing()
-
- if self.other_depends:
- print
- print "*** binary dependencies ***"
- print "Your executable(s) also depend on these dlls which are not included,"
- print "you may or may not need to distribute them."
- print
- print "Make sure you have the license if you distribute any of them, and"
- print "make sure you don't distribute files belonging to the operating system."
- print
- for fnm in self.other_depends:
- print " ", os.path.basename(fnm), "-", fnm.strip()
-
- def create_modulefinder(self):
- from modulefinder import ReplacePackage
- from py2exe.mf import ModuleFinder
- ReplacePackage("_xmlplus", "xml")
- return ModuleFinder(excludes=self.excludes, skip_scan=self.skip_scan)
-
- def fix_badmodules(self, mf):
- # This dictionary maps additional builtin module names to the
- # module that creates them.
- # For example, 'wxPython.misc' creates a builtin module named
- # 'miscc'.
- builtins = {"clip_dndc": "wxPython.clip_dnd",
- "cmndlgsc": "wxPython.cmndlgs",
- "controls2c": "wxPython.controls2",
- "controlsc": "wxPython.controls",
- "eventsc": "wxPython.events",
- "filesysc": "wxPython.filesys",
- "fontsc": "wxPython.fonts",
- "framesc": "wxPython.frames",
- "gdic": "wxPython.gdi",
- "imagec": "wxPython.image",
- "mdic": "wxPython.mdi",
- "misc2c": "wxPython.misc2",
- "miscc": "wxPython.misc",
- "printfwc": "wxPython.printfw",
- "sizersc": "wxPython.sizers",
- "stattoolc": "wxPython.stattool",
- "streamsc": "wxPython.streams",
- "utilsc": "wxPython.utils",
- "windows2c": "wxPython.windows2",
- "windows3c": "wxPython.windows3",
- "windowsc": "wxPython.windows",
- }
-
- # Somewhat hackish: change modulefinder's badmodules dictionary in place.
- bad = mf.badmodules
- # mf.badmodules is a dictionary mapping unfound module names
- # to another dictionary, the keys of this are the module names
- # importing the unknown module. For the 'miscc' module
- # mentioned above, it looks like this:
- # mf.badmodules["miscc"] = { "wxPython.miscc": 1 }
- for name in mf.any_missing():
- if name in self.ignores:
- del bad[name]
- continue
- mod = builtins.get(name, None)
- if mod is not None:
- if mod in bad[name] and bad[name] == {mod: 1}:
- del bad[name]
-
- def find_dlls(self, extensions):
- dlls = [item.__file__ for item in extensions]
-## extra_path = ["."] # XXX
- extra_path = []
- dlls, unfriendly_dlls, other_depends = \
- self.find_dependend_dlls(dlls,
- extra_path + sys.path,
- self.dll_excludes)
- self.other_depends = other_depends
- # dlls contains the path names of all dlls we need.
- # If a dll uses a function PyImport_ImportModule (or what was it?),
- # it's name is additionally in unfriendly_dlls.
- for item in extensions:
- if item.__file__ in dlls:
- dlls.remove(item.__file__)
- return dlls
-
- def create_directories(self):
- bdist_base = self.get_finalized_command('bdist').bdist_base
- self.bdist_dir = os.path.join(bdist_base, 'winexe')
-
- collect_name = "collect-%d.%d" % sys.version_info[:2]
- self.collect_dir = os.path.abspath(os.path.join(self.bdist_dir, collect_name))
- self.mkpath(self.collect_dir)
-
- bundle_name = "bundle-%d.%d" % sys.version_info[:2]
- self.bundle_dir = os.path.abspath(os.path.join(self.bdist_dir, bundle_name))
- self.mkpath(self.bundle_dir)
-
- self.temp_dir = os.path.abspath(os.path.join(self.bdist_dir, "temp"))
- self.mkpath(self.temp_dir)
-
- self.dist_dir = os.path.abspath(self.dist_dir)
- self.mkpath(self.dist_dir)
-
- if self.distribution.zipfile is None:
- self.lib_dir = self.dist_dir
- else:
- self.lib_dir = os.path.join(self.dist_dir,
- os.path.dirname(self.distribution.zipfile))
- self.mkpath(self.lib_dir)
-
- def copy_extensions(self, extensions):
- print "*** copy extensions ***"
- # copy the extensions to the target directory
- for item in extensions:
- src = item.__file__
- if self.bundle_files > 2: # don't bundle pyds and dlls
- dst = os.path.join(self.lib_dir, (item.__pydfile__))
- self.copy_file(src, dst, preserve_mode=0)
- self.lib_files.append(dst)
- else:
- # we have to preserve the packages
- package = "\\".join(item.__name__.split(".")[:-1])
- if package:
- dst = os.path.join(package, os.path.basename(src))
- else:
- dst = os.path.basename(src)
- self.copy_file(src, os.path.join(self.collect_dir, dst), preserve_mode=0)
- self.compiled_files.append(dst)
-
- def copy_dlls(self, dlls):
- # copy needed dlls where they belong.
- print "*** copy dlls ***"
- if self.bundle_files < 3:
- self.copy_dlls_bundle_files(dlls)
- return
- # dlls belong into the lib_dir, except those listed in dlls_in_exedir,
- # which have to go into exe_dir (pythonxy.dll, w9xpopen.exe).
- for dll in dlls:
- base = os.path.basename(dll)
- if base.lower() in self.dlls_in_exedir:
- # These special dlls cannot be in the lib directory,
- # they must go into the exe directory.
- dst = os.path.join(self.exe_dir, base)
- else:
- dst = os.path.join(self.lib_dir, base)
- _, copied = self.copy_file(dll, dst, preserve_mode=0)
- if not self.dry_run and copied and base.lower() == python_dll.lower():
- # If we actually copied pythonxy.dll, we have to patch it.
- #
- # Previously, the code did it every time, but this
- # breaks if, for example, someone runs UPX over the
- # dist directory. Patching an UPX'd dll seems to work
- # (no error is detected when patching), but the
- # resulting dll does not work anymore.
- #
- # The function restores the file times so
- # dependencies still work correctly.
- self.patch_python_dll_winver(dst)
-
- self.lib_files.append(dst)
-
- def copy_dlls_bundle_files(self, dlls):
- # If dlls have to be bundled, they are copied into the
- # collect_dir and will be added to the list of files to
- # include in the zip archive 'self.compiled_files'.
- #
- # dlls listed in dlls_in_exedir have to be treated differently:
- #
- for dll in dlls:
- base = os.path.basename(dll)
- if base.lower() in self.dlls_in_exedir:
- # pythonXY.dll must be bundled as resource.
- # w9xpopen.exe must be copied to self.exe_dir.
- if base.lower() == python_dll.lower() and self.bundle_files < 2:
- dst = os.path.join(self.bundle_dir, base)
- else:
- dst = os.path.join(self.exe_dir, base)
- _, copied = self.copy_file(dll, dst, preserve_mode=0)
- if not self.dry_run and copied and base.lower() == python_dll.lower():
- # If we actually copied pythonxy.dll, we have to
- # patch it. Well, since it's impossible to load
- # resources from the bundled dlls it probably
- # doesn't matter.
- self.patch_python_dll_winver(dst)
- self.lib_files.append(dst)
- continue
-
- dst = os.path.join(self.collect_dir, os.path.basename(dll))
- self.copy_file(dll, dst, preserve_mode=0)
- # Make sure they will be included into the zipfile.
- self.compiled_files.append(os.path.basename(dst))
-
- def create_binaries(self, py_files, extensions, dlls):
- dist = self.distribution
-
- # byte compile the python modules into the target directory
- print "*** byte compile python files ***"
- self.compiled_files = byte_compile(py_files,
- target_dir=self.collect_dir,
- optimize=self.optimize,
- force=0,
- verbose=self.verbose,
- dry_run=self.dry_run)
-
- self.lib_files = []
- self.console_exe_files = []
- self.windows_exe_files = []
- self.service_exe_files = []
- self.comserver_files = []
-
- self.copy_extensions(extensions)
- self.copy_dlls(dlls)
-
- # create the shared zipfile containing all Python modules
- if dist.zipfile is None:
- fd, archive_name = tempfile.mkstemp()
- os.close(fd)
- else:
- archive_name = os.path.join(self.lib_dir,
- os.path.basename(dist.zipfile))
-
- arcname = self.make_lib_archive(archive_name,
- base_dir=self.collect_dir,
- files=self.compiled_files,
- verbose=self.verbose,
- dry_run=self.dry_run)
- if dist.zipfile is not None:
- self.lib_files.append(arcname)
-
- for target in self.distribution.isapi:
- print "*** copy isapi support DLL ***"
- # Locate the support DLL, and copy as "_script.dll", just like
- # isapi itself
- import isapi
- src_name = is_debug_build and "PyISAPI_loader_d.dll" or \
- "PyISAPI_loader.dll"
- src = os.path.join(isapi.__path__[0], src_name)
- # destination name is "_{module_name}.dll" just like pyisapi does.
- script_base = os.path.splitext(os.path.basename(target.script))[0]
- dst = os.path.join(self.exe_dir, "_" + script_base + ".dll")
- self.copy_file(src, dst, preserve_mode=0)
-
- if self.distribution.has_data_files():
- print "*** copy data files ***"
- install_data = self.reinitialize_command('install_data')
- install_data.install_dir = self.dist_dir
- install_data.ensure_finalized()
- install_data.run()
-
- self.lib_files.extend(install_data.get_outputs())
-
- # build the executables
- for target in dist.console:
- dst = self.build_executable(target, self.get_console_template(),
- arcname, target.script)
- self.console_exe_files.append(dst)
- for target in dist.windows:
- dst = self.build_executable(target, self.get_windows_template(),
- arcname, target.script)
- self.windows_exe_files.append(dst)
- for target in dist.service:
- dst = self.build_service(target, self.get_service_template(),
- arcname)
- self.service_exe_files.append(dst)
-
- for target in dist.isapi:
- dst = self.build_isapi(target, self.get_isapi_template(), arcname)
-
- for target in dist.com_server:
- if getattr(target, "create_exe", True):
- dst = self.build_comserver(target, self.get_comexe_template(),
- arcname)
- self.comserver_files.append(dst)
- if getattr(target, "create_dll", True):
- dst = self.build_comserver(target, self.get_comdll_template(),
- arcname)
- self.comserver_files.append(dst)
-
- for target in dist.ctypes_com_server:
- dst = self.build_comserver(target, self.get_ctypes_comdll_template(),
- arcname, boot_script="ctypes_com_server")
- self.comserver_files.append(dst)
-
- if dist.zipfile is None:
- os.unlink(arcname)
- else:
- if self.bundle_files < 3 or self.compressed:
- arcbytes = open(arcname, "rb").read()
- arcfile = open(arcname, "wb")
-
- if self.bundle_files < 2: # bundle pythonxy.dll also
- print "Adding %s to %s" % (python_dll, arcname)
- arcfile.write("")
- bytes = open(os.path.join(self.bundle_dir, python_dll), "rb").read()
- arcfile.write(struct.pack("i", len(bytes)))
- arcfile.write(bytes) # python dll
-
- if self.compressed:
- # prepend zlib.pyd also
- zlib_file = imp.find_module("zlib")[0]
- if zlib_file:
- print "Adding zlib%s.pyd to %s" % (is_debug_build and "_d" or "", arcname)
- arcfile.write("")
- bytes = zlib_file.read()
- arcfile.write(struct.pack("i", len(bytes)))
- arcfile.write(bytes) # zlib.pyd
-
- arcfile.write(arcbytes)
-
-#### if self.bundle_files < 2:
-#### # remove python dll from the exe_dir, since it is now bundled.
-#### os.remove(os.path.join(self.exe_dir, python_dll))
-
-
- # for user convenience, let subclasses override the templates to use
- def get_console_template(self):
- return is_debug_build and "run_d.exe" or "run.exe"
-
- def get_windows_template(self):
- return is_debug_build and "run_w_d.exe" or "run_w.exe"
-
- def get_service_template(self):
- return is_debug_build and "run_d.exe" or "run.exe"
-
- def get_isapi_template(self):
- return is_debug_build and "run_isapi_d.dll" or "run_isapi.dll"
-
- def get_comexe_template(self):
- return is_debug_build and "run_w_d.exe" or "run_w.exe"
-
- def get_comdll_template(self):
- return is_debug_build and "run_dll_d.dll" or "run_dll.dll"
-
- def get_ctypes_comdll_template(self):
- return is_debug_build and "run_ctypes_dll_d.dll" or "run_ctypes_dll.dll"
-
- def fixup_distribution(self):
- dist = self.distribution
-
- # Convert our args into target objects.
- dist.com_server = FixupTargets(dist.com_server, "modules")
- dist.ctypes_com_server = FixupTargets(dist.ctypes_com_server, "modules")
- dist.service = FixupTargets(dist.service, "modules")
- dist.windows = FixupTargets(dist.windows, "script")
- dist.console = FixupTargets(dist.console, "script")
- dist.isapi = FixupTargets(dist.isapi, "script")
-
- # make sure all targets use the same directory, this is
- # also the directory where the pythonXX.dll must reside
- paths = sets.Set()
- for target in dist.com_server + dist.service \
- + dist.windows + dist.console + dist.isapi:
- paths.add(os.path.dirname(target.get_dest_base()))
-
- if len(paths) > 1:
- raise DistutilsOptionError, \
- "all targets must use the same directory: %s" % \
- [p for p in paths]
- if paths:
- exe_dir = paths.pop() # the only element
- if os.path.isabs(exe_dir):
- raise DistutilsOptionError, \
- "exe directory must be relative: %s" % exe_dir
- self.exe_dir = os.path.join(self.dist_dir, exe_dir)
- self.mkpath(self.exe_dir)
- else:
- # Do we allow to specify no targets?
- # We can at least build a zipfile...
- self.exe_dir = self.lib_dir
-
- def get_boot_script(self, boot_type):
- # return the filename of the script to use for com servers.
- thisfile = sys.modules['py2exe.build_exe'].__file__
- return os.path.join(os.path.dirname(thisfile),
- "boot_" + boot_type + ".py")
-
- def build_comserver(self, target, template, arcname, boot_script="com_servers"):
- # Build a dll and an exe executable hosting all the com
- # objects listed in module_names.
- # The basename of the dll/exe is the last part of the first module.
- # Do we need a way to specify the name of the files to be built?
-
- # Setup the variables our boot script needs.
- vars = {"com_module_names" : target.modules}
- boot = self.get_boot_script(boot_script)
- # and build it
- return self.build_executable(target, template, arcname, boot, vars)
-
- def get_service_names(self, module_name):
- # import the script with every side effect :)
- __import__(module_name)
- mod = sys.modules[module_name]
- for name, klass in mod.__dict__.items():
- if hasattr(klass, "_svc_name_"):
- break
- else:
- raise RuntimeError, "No services in module"
- deps = ()
- if hasattr(klass, "_svc_deps_"):
- deps = klass._svc_deps_
- return klass.__name__, klass._svc_name_, klass._svc_display_name_, deps
-
- def build_service(self, target, template, arcname):
- # It should be possible to host many modules in a single service -
- # but this is yet to be tested.
- assert len(target.modules)==1, "We only support one service module"
-
- cmdline_style = getattr(target, "cmdline_style", "py2exe")
- if cmdline_style not in ["py2exe", "pywin32", "custom"]:
- raise RuntimeError, "cmdline_handler invalid"
-
- vars = {"service_module_names" : target.modules,
- "cmdline_style": cmdline_style,
- }
- boot = self.get_boot_script("service")
- return self.build_executable(target, template, arcname, boot, vars)
-
- def build_isapi(self, target, template, arcname):
- target_module = os.path.splitext(os.path.basename(target.script))[0]
- vars = {"isapi_module_name" : target_module,
- }
- return self.build_executable(target, template, arcname, None, vars)
-
- def build_manifest(self, target, template):
- # Optionally return a manifest to be included in the target executable.
- # Note for Python 2.6 and later, its *necessary* to include a manifest
- # which correctly references the CRT. For earlier versions, a manifest
- # is optional, and only necessary to customize things like
- # Vista's User Access Control 'requestedExecutionLevel' setting, etc.
- default_manifest = """
-
-
-
-
-
-
-
-
-
- """
- from py2exe_util import load_resource
- if os.path.splitext(template)[1]==".exe":
- rid = 1
- else:
- rid = 2
- try:
- # Manfiests have resource type of 24, and ID of either 1 or 2.
- mfest = load_resource(ensure_unicode(template), RT_MANIFEST, rid)
- # we consider the manifest 'changed' as we know we clobber all
- # resources including the existing manifest - so this manifest must
- # get written even if we make no other changes.
- changed = True
- except RuntimeError:
- mfest = default_manifest
- # in this case the template had no existing manifest, so its
- # not considered 'changed' unless we make further changes later.
- changed = False
- # update the manifest according to our options.
- # for now a regex will do.
- if target.uac_info:
- changed = True
- if isinstance(target.uac_info, tuple):
- exec_level, ui = target.uac_info
- else:
- exec_level = target.uac_info
- ui = False
- new_lines = []
- for line in mfest.splitlines():
- repl = r'\1%s\3%s\5' % (exec_level, ui)
- new_lines.append(re.sub(pat_manifest_uac, repl, line))
- mfest = "".join(new_lines)
- if not changed:
- return None, None
- return mfest, rid
-
- def build_executable(self, target, template, arcname, script, vars={}):
- # Build an executable for the target
- # template is the exe-stub to use, and arcname is the zipfile
- # containing the python modules.
- from py2exe_util import add_resource, add_icon
- ext = os.path.splitext(template)[1]
- exe_base = target.get_dest_base()
- exe_path = os.path.join(self.dist_dir, exe_base + ext)
- # The user may specify a sub-directory for the exe - that's fine, we
- # just specify the parent directory for the .zip
- parent_levels = len(os.path.normpath(exe_base).split(os.sep))-1
- lib_leaf = self.lib_dir[len(self.dist_dir)+1:]
- relative_arcname = ((".." + os.sep) * parent_levels)
- if lib_leaf: relative_arcname += lib_leaf + os.sep
- relative_arcname += os.path.basename(arcname)
-
- src = os.path.join(os.path.dirname(__file__), template)
- # We want to force the creation of this file, as otherwise distutils
- # will see the earlier time of our 'template' file versus the later
- # time of our modified template file, and consider our old file OK.
- old_force = self.force
- self.force = True
- self.copy_file(src, exe_path, preserve_mode=0)
- self.force = old_force
-
- # Make sure the file is writeable...
- os.chmod(exe_path, stat.S_IREAD | stat.S_IWRITE)
- try:
- f = open(exe_path, "a+b")
- f.close()
- except IOError, why:
- print "WARNING: File %s could not be opened - %s" % (exe_path, why)
-
- # We create a list of code objects, and write it as a marshaled
- # stream. The framework code then just exec's these in order.
- # First is our common boot script.
- boot = self.get_boot_script("common")
- boot_code = compile(file(boot, "U").read(),
- os.path.abspath(boot), "exec")
- code_objects = [boot_code]
- if self.bundle_files < 3:
- code_objects.append(
- compile("import zipextimporter; zipextimporter.install()",
- "", "exec"))
- for var_name, var_val in vars.items():
- code_objects.append(
- compile("%s=%r\n" % (var_name, var_val), var_name, "exec")
- )
- if self.custom_boot_script:
- code_object = compile(file(self.custom_boot_script, "U").read() + "\n",
- os.path.abspath(self.custom_boot_script), "exec")
- code_objects.append(code_object)
- if script:
- code_object = compile(open(script, "U").read() + "\n",
- os.path.basename(script), "exec")
- code_objects.append(code_object)
- code_bytes = marshal.dumps(code_objects)
-
- if self.distribution.zipfile is None:
- relative_arcname = ""
-
- si = struct.pack("iiii",
- 0x78563412, # a magic value,
- self.optimize,
- self.unbuffered,
- len(code_bytes),
- ) + relative_arcname + "\000"
-
- script_bytes = si + code_bytes + '\000\000'
- self.announce("add script resource, %d bytes" % len(script_bytes))
- if not self.dry_run:
- add_resource(ensure_unicode(exe_path), script_bytes, u"PYTHONSCRIPT", 1, True)
-
- # add the pythondll as resource, and delete in self.exe_dir
- if self.bundle_files < 2 and self.distribution.zipfile is None:
- # bundle pythonxy.dll
- dll_path = os.path.join(self.bundle_dir, python_dll)
- bytes = open(dll_path, "rb").read()
- # image, bytes, lpName, lpType
-
- print "Adding %s as resource to %s" % (python_dll, exe_path)
- add_resource(ensure_unicode(exe_path), bytes,
- # for some reason, the 3. argument MUST BE UPPER CASE,
- # otherwise the resource will not be found.
- ensure_unicode(python_dll).upper(), 1, False)
-
- if self.compressed and self.bundle_files < 3 and self.distribution.zipfile is None:
- zlib_file = imp.find_module("zlib")[0]
- if zlib_file:
- print "Adding zlib.pyd as resource to %s" % exe_path
- zlib_bytes = zlib_file.read()
- add_resource(ensure_unicode(exe_path), zlib_bytes,
- # for some reason, the 3. argument MUST BE UPPER CASE,
- # otherwise the resource will not be found.
- u"ZLIB.PYD", 1, False)
-
- # Handle all resources specified by the target
- bitmap_resources = getattr(target, "bitmap_resources", [])
- for bmp_id, bmp_filename in bitmap_resources:
- bmp_data = open(bmp_filename, "rb").read()
- # skip the 14 byte bitmap header.
- if not self.dry_run:
- add_resource(ensure_unicode(exe_path), bmp_data[14:], RT_BITMAP, bmp_id, False)
- icon_resources = getattr(target, "icon_resources", [])
- for ico_id, ico_filename in icon_resources:
- if not self.dry_run:
- add_icon(ensure_unicode(exe_path), ensure_unicode(ico_filename), ico_id)
-
- # a manifest
- mfest, mfest_id = self.build_manifest(target, src)
- if mfest:
- self.announce("add manifest, %d bytes" % len(mfest))
- if not self.dry_run:
- add_resource(ensure_unicode(exe_path), mfest, RT_MANIFEST, mfest_id, False)
-
- for res_type, res_id, data in getattr(target, "other_resources", []):
- if not self.dry_run:
- if isinstance(res_type, basestring):
- res_type = ensure_unicode(res_type)
- add_resource(ensure_unicode(exe_path), data, res_type, res_id, False)
-
- typelib = getattr(target, "typelib", None)
- if typelib is not None:
- data = open(typelib, "rb").read()
- add_resource(ensure_unicode(exe_path), data, u"TYPELIB", 1, False)
-
- self.add_versioninfo(target, exe_path)
-
- # Hm, this doesn't make sense with normal executables, which are
- # already small (around 20 kB).
- #
- # But it would make sense with static build pythons, but not
- # if the zipfile is appended to the exe - it will be too slow
- # then (although it is a wonder it works at all in this case).
- #
- # Maybe it would be faster to use the frozen modules machanism
- # instead of the zip-import?
-## if self.compressed:
-## import gc
-## gc.collect() # to close all open files!
-## os.system("upx -9 %s" % exe_path)
-
- if self.distribution.zipfile is None:
- zip_data = open(arcname, "rb").read()
- open(exe_path, "a+b").write(zip_data)
-
- return exe_path
-
- def add_versioninfo(self, target, exe_path):
- # Try to build and add a versioninfo resource
-
- def get(name, md = self.distribution.metadata):
- # Try to get an attribute from the target, if not defined
- # there, from the distribution's metadata, or None. Note
- # that only *some* attributes are allowed by distutils on
- # the distribution's metadata: version, description, and
- # name.
- return getattr(target, name, getattr(md, name, None))
-
- version = get("version")
- if version is None:
- return
-
- from py2exe.resources.VersionInfo import Version, RT_VERSION, VersionError
- version = Version(version,
- file_description = get("description"),
- comments = get("comments"),
- company_name = get("company_name"),
- legal_copyright = get("copyright"),
- legal_trademarks = get("trademarks"),
- original_filename = os.path.basename(exe_path),
- product_name = get("name"),
- product_version = get("product_version") or version)
- try:
- bytes = version.resource_bytes()
- except VersionError, detail:
- self.warn("Version Info will not be included:\n %s" % detail)
- return
-
- from py2exe_util import add_resource
- add_resource(ensure_unicode(exe_path), bytes, RT_VERSION, 1, False)
-
- def patch_python_dll_winver(self, dll_name, new_winver = None):
- from py2exe.resources.StringTables import StringTable, RT_STRING
- from py2exe_util import add_resource, load_resource
- from py2exe.resources.VersionInfo import RT_VERSION
-
- new_winver = new_winver or self.distribution.metadata.name or "py2exe"
- if self.verbose:
- print "setting sys.winver for '%s' to '%s'" % (dll_name, new_winver)
- if self.dry_run:
- return
-
- # We preserve the times on the file, so the dependency tracker works.
- st = os.stat(dll_name)
- # and as the resource functions silently fail if the open fails,
- # check it explicitly.
- os.chmod(dll_name, stat.S_IREAD | stat.S_IWRITE)
- try:
- f = open(dll_name, "a+b")
- f.close()
- except IOError, why:
- print "WARNING: File %s could not be opened - %s" % (dll_name, why)
- # We aren't smart enough to update string resources in place, so we need
- # to persist other resources we care about.
- unicode_name = ensure_unicode(dll_name)
-
- # Preserve existing version info (all versions should have this)
- ver_info = load_resource(unicode_name, RT_VERSION, 1)
- # Preserve an existing manifest (only python26.dll+ will have this)
- try:
- # Manfiests have resource type of 24, and ID of either 1 or 2.
- mfest = load_resource(unicode_name, RT_MANIFEST, 2)
- except RuntimeError:
- mfest = None
-
- # Start putting the resources back, passing 'delete=True' for the first.
- add_resource(unicode_name, ver_info, RT_VERSION, 1, True)
- if mfest is not None:
- add_resource(unicode_name, mfest, RT_MANIFEST, 2, False)
-
- # OK - do the strings.
- s = StringTable()
- # 1000 is the resource ID Python loads for its winver.
- s.add_string(1000, new_winver)
- for id, data in s.binary():
- add_resource(ensure_unicode(dll_name), data, RT_STRING, id, False)
-
- # restore the time.
- os.utime(dll_name, (st[stat.ST_ATIME], st[stat.ST_MTIME]))
-
- def find_dependend_dlls(self, dlls, pypath, dll_excludes):
- import py2exe_util
- sysdir = py2exe_util.get_sysdir()
- windir = py2exe_util.get_windir()
- # This is the tail of the path windows uses when looking for dlls
- # XXX On Windows NT, the SYSTEM directory is also searched
- exedir = os.path.dirname(sys.executable)
- syspath = os.environ['PATH']
- loadpath = ';'.join([exedir, sysdir, windir, syspath])
-
- # Found by Duncan Booth:
- # It may be possible that bin_depends needs extension modules,
- # so the loadpath must be extended by our python path.
- loadpath = loadpath + ';' + ';'.join(pypath)
-
- templates = sets.Set()
- if self.distribution.console:
- templates.add(self.get_console_template())
- if self.distribution.windows:
- templates.add(self.get_windows_template())
- if self.distribution.service:
- templates.add(self.get_service_template())
- for target in self.distribution.com_server:
- if getattr(target, "create_exe", True):
- templates.add(self.get_comexe_template())
- if getattr(target, "create_dll", True):
- templates.add(self.get_comdll_template())
-
- templates = [os.path.join(os.path.dirname(__file__), t) for t in templates]
-
- # We use Python.exe to track the dependencies of our run stubs ...
- images = dlls + templates
-
- self.announce("Resolving binary dependencies:")
- excludes_use = dll_excludes[:]
- # The MSVCRT modules are never found when using VS2008+
- if sys.version_info > (2,6):
- excludes_use.append("msvcr90.dll")
-
- # we add python.exe (aka sys.executable) to the list of images
- # to scan for dependencies, but remove it later again from the
- # results list. In this way pythonXY.dll is collected, and
- # also the libraries it depends on.
- alldlls, warnings, other_depends = \
- bin_depends(loadpath, images + [sys.executable], excludes_use)
- alldlls.remove(sys.executable)
- for dll in alldlls:
- self.announce(" %s" % dll)
- # ... but we don't need the exe stubs run_xxx.exe
- for t in templates:
- alldlls.remove(t)
-
- return alldlls, warnings, other_depends
- # find_dependend_dlls()
-
- def get_hidden_imports(self):
- # imports done from builtin modules in C code (untrackable by py2exe)
- return {"time": ["_strptime"],
-## "datetime": ["time"],
- "cPickle": ["copy_reg"],
- "parser": ["copy_reg"],
- "codecs": ["encodings"],
-
- "cStringIO": ["copy_reg"],
- "_sre": ["copy", "string", "sre"],
- }
-
- def parse_mf_results(self, mf):
- for name, imports in self.get_hidden_imports().items():
- if name in mf.modules.keys():
- for mod in imports:
- mf.import_hook(mod)
-
- tcl_src_dir = tcl_dst_dir = None
- if "Tkinter" in mf.modules.keys():
- import Tkinter
- import _tkinter
- tk = _tkinter.create()
- tcl_dir = tk.call("info", "library")
- tcl_src_dir = os.path.split(tcl_dir)[0]
- tcl_dst_dir = os.path.join(self.lib_dir, "tcl")
-
- self.announce("Copying TCL files from %s..." % tcl_src_dir)
- self.copy_tree(os.path.join(tcl_src_dir, "tcl%s" % _tkinter.TCL_VERSION),
- os.path.join(tcl_dst_dir, "tcl%s" % _tkinter.TCL_VERSION))
- self.copy_tree(os.path.join(tcl_src_dir, "tk%s" % _tkinter.TK_VERSION),
- os.path.join(tcl_dst_dir, "tk%s" % _tkinter.TK_VERSION))
- del tk, _tkinter, Tkinter
-
- # Retrieve modules from modulefinder
- py_files = []
- extensions = []
- builtins = []
-
- for item in mf.modules.values():
- # There may be __main__ modules (from mf.run_script), but
- # we don't need them in the zipfile we build.
- if item.__name__ == "__main__":
- continue
- if self.bundle_files < 3 and item.__name__ in ("pythoncom", "pywintypes"):
- # these are handled specially in zipextimporter.
- continue
- src = item.__file__
- if src:
- base, ext = os.path.splitext(src)
- suffix = ext
- if sys.platform.startswith("win") and ext in [".dll", ".pyd"] \
- and base.endswith("_d"):
- suffix = "_d" + ext
-
- if suffix in _py_suffixes:
- py_files.append(item)
- elif suffix in _c_suffixes:
- extensions.append(item)
- if not self.bundle_files < 3:
- loader = self.create_loader(item)
- if loader:
- py_files.append(loader)
- else:
- raise RuntimeError \
- ("Don't know how to handle '%s'" % repr(src))
- else:
- builtins.append(item.__name__)
-
- # sort on the file names, the output is nicer to read
- py_files.sort(lambda a, b: cmp(a.__file__, b.__file__))
- extensions.sort(lambda a, b: cmp(a.__file__, b.__file__))
- builtins.sort()
- return py_files, extensions, builtins
-
- def plat_finalize(self, modules, py_files, extensions, dlls):
- # platform specific code for final adjustments to the file
- # lists
- if sys.platform == "win32":
- # pythoncom and pywintypes are imported via LoadLibrary calls,
- # help py2exe to include the dlls:
- if "pythoncom" in modules.keys():
- import pythoncom
- dlls.add(pythoncom.__file__)
- if "pywintypes" in modules.keys():
- import pywintypes
- dlls.add(pywintypes.__file__)
- self.copy_w9xpopen(modules, dlls)
- else:
- raise DistutilsError, "Platform %s not yet implemented" % sys.platform
-
- def copy_w9xpopen(self, modules, dlls):
- # Using popen requires (on Win9X) the w9xpopen.exe helper executable.
- if "os" in modules.keys() or "popen2" in modules.keys():
- if is_debug_build:
- fname = os.path.join(os.path.dirname(sys.executable), "w9xpopen_d.exe")
- else:
- fname = os.path.join(os.path.dirname(sys.executable), "w9xpopen.exe")
- # Don't copy w9xpopen.exe if it doesn't exist (64-bit
- # Python build, for example)
- if os.path.exists(fname):
- dlls.add(fname)
-
- def create_loader(self, item):
- # Hm, how to avoid needless recreation of this file?
- pathname = os.path.join(self.temp_dir, "%s.py" % item.__name__)
- if self.bundle_files > 2: # don't bundle pyds and dlls
- # all dlls are copied into the same directory, so modify
- # names to include the package name to avoid name
- # conflicts and tuck it away for future reference
- fname = item.__name__ + os.path.splitext(item.__file__)[1]
- item.__pydfile__ = fname
- else:
- fname = os.path.basename(item.__file__)
-
- # and what about dry_run?
- if self.verbose:
- print "creating python loader for extension '%s' (%s -> %s)" % (item.__name__,item.__file__,fname)
-
- source = LOADER % fname
- if not self.dry_run:
- open(pathname, "w").write(source)
- else:
- return None
- from modulefinder import Module
- return Module(item.__name__, pathname)
-
- def plat_prepare(self):
- self.includes.append("warnings") # needed by Python itself
- if not self.ascii:
- self.packages.append("encodings")
- self.includes.append("codecs")
- if self.bundle_files < 3:
- self.includes.append("zipextimporter")
- self.excludes.append("_memimporter") # builtin in run_*.exe and run_*.dll
- if self.compressed:
- self.includes.append("zlib")
-
- # os.path will never be found ;-)
- self.ignores.append('os.path')
-
- # update the self.ignores list to ignore platform specific
- # modules.
- if sys.platform == "win32":
- self.ignores += ['AL',
- 'Audio_mac',
- 'Carbon.File',
- 'Carbon.Folder',
- 'Carbon.Folders',
- 'EasyDialogs',
- 'MacOS',
- 'Mailman',
- 'SOCKS',
- 'SUNAUDIODEV',
- '_dummy_threading',
- '_emx_link',
- '_xmlplus',
- '_xmlrpclib',
- 'al',
- 'bundlebuilder',
- 'ce',
- 'cl',
- 'dbm',
- 'dos',
- 'fcntl',
- 'gestalt',
- 'grp',
- 'ic',
- 'java.lang',
- 'mac',
- 'macfs',
- 'macostools',
- 'mkcwproject',
- 'org.python.core',
- 'os.path',
- 'os2',
- 'poll',
- 'posix',
- 'pwd',
- 'readline',
- 'riscos',
- 'riscosenviron',
- 'riscospath',
- 'rourl2path',
- 'sgi',
- 'sgmlop',
- 'sunaudiodev',
- 'termios',
- 'vms_lib']
- # special dlls which must be copied to the exe_dir, not the lib_dir
- self.dlls_in_exedir = [python_dll,
- "w9xpopen%s.exe" % (is_debug_build and "_d" or ""),
- "msvcr71%s.dll" % (is_debug_build and "d" or "")]
- else:
- raise DistutilsError, "Platform %s not yet implemented" % sys.platform
-
- def find_needed_modules(self, mf, files, modules):
- # feed Modulefinder with everything, and return it.
- for mod in modules:
- mf.import_hook(mod)
-
- for path in files:
- mf.run_script(path)
-
- mf.run_script(self.get_boot_script("common"))
-
- if self.distribution.com_server:
- mf.run_script(self.get_boot_script("com_servers"))
-
- if self.distribution.ctypes_com_server:
- mf.run_script(self.get_boot_script("ctypes_com_server"))
-
- if self.distribution.service:
- mf.run_script(self.get_boot_script("service"))
-
- if self.custom_boot_script:
- mf.run_script(self.custom_boot_script)
-
- for mod in self.includes:
- if mod[-2:] == '.*':
- mf.import_hook(mod[:-2], None, ['*'])
- else:
- mf.import_hook(mod)
-
- for f in self.packages:
- def visit(arg, dirname, names):
- if '__init__.py' in names:
- arg.append(dirname)
-
- # Try to find the package using ModuleFinders's method to
- # allow for modulefinder.AddPackagePath interactions
- mf.import_hook(f)
-
- # If modulefinder has seen a reference to the package, then
- # we prefer to believe that (imp_find_module doesn't seem to locate
- # sub-packages)
- if f in mf.modules:
- module = mf.modules[f]
- if module.__path__ is None:
- # it's a module, not a package, so paths contains just the
- # file entry
- paths = [module.__file__]
- else:
- # it is a package because __path__ is available. __path__
- # is actually a list of paths that are searched to import
- # sub-modules and sub-packages
- paths = module.__path__
- else:
- # Find path of package
- try:
- paths = [imp_find_module(f)[1]]
- except ImportError:
- self.warn("No package named %s" % f)
- continue
-
- packages = []
- for path in paths:
- # walk the path to find subdirs containing __init__.py files
- os.path.walk(path, visit, packages)
-
- # scan the results (directory of __init__.py files)
- # first trim the path (of the head package),
- # then convert directory name in package name,
- # finally push into modulefinder.
- for p in packages:
- if p.startswith(path):
- package = f + '.' + p[len(path)+1:].replace('\\', '.')
- mf.import_hook(package, None, ["*"])
-
- return mf
-
- def make_lib_archive(self, zip_filename, base_dir, files,
- verbose=0, dry_run=0):
- from distutils.dir_util import mkpath
- if not self.skip_archive:
- # Like distutils "make_archive", but we can specify the files
- # to include, and the compression to use - default is
- # ZIP_STORED to keep the runtime performance up. Also, we
- # don't append '.zip' to the filename.
- mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
-
- if self.compressed:
- compression = zipfile.ZIP_DEFLATED
- else:
- compression = zipfile.ZIP_STORED
-
- if not dry_run:
- z = zipfile.ZipFile(zip_filename, "w",
- compression=compression)
- for f in files:
- z.write(os.path.join(base_dir, f), f)
- z.close()
-
- return zip_filename
- else:
- # Don't really produce an archive, just copy the files.
- from distutils.file_util import copy_file
-
- destFolder = os.path.dirname(zip_filename)
-
- for f in files:
- d = os.path.dirname(f)
- if d:
- mkpath(os.path.join(destFolder, d), verbose=verbose, dry_run=dry_run)
- copy_file(
- os.path.join(base_dir, f),
- os.path.join(destFolder, f),
- preserve_mode=0,
- verbose=verbose,
- dry_run=dry_run
- )
- return '.'
-
-
-################################################################
-
-class FileSet:
- # A case insensitive but case preserving set of files
- def __init__(self, iterable=None):
- self._dict = {}
- if iterable is not None:
- for arg in iterable:
- self.add(arg)
-
- def __repr__(self):
- return "" % (self._dict.values(), id(self))
-
- def add(self, fname):
- self._dict[fname.upper()] = fname
-
- def remove(self, fname):
- del self._dict[fname.upper()]
-
- def __contains__(self, fname):
- return fname.upper() in self._dict.keys()
-
- def __getitem__(self, index):
- key = self._dict.keys()[index]
- return self._dict[key]
-
- def __len__(self):
- return len(self._dict)
-
- def copy(self):
- res = FileSet()
- res._dict.update(self._dict)
- return res
-
-# class FileSet()
-
-def bin_depends(path, images, excluded_dlls):
- import py2exe_util
- warnings = FileSet()
- images = FileSet(images)
- dependents = FileSet()
- others = FileSet()
- while images:
- for image in images.copy():
- images.remove(image)
- if not image in dependents:
- dependents.add(image)
- abs_image = os.path.abspath(image)
- loadpath = os.path.dirname(abs_image) + ';' + path
- for result in py2exe_util.depends(image, loadpath).items():
- dll, uses_import_module = result
- if os.path.basename(dll).lower() not in excluded_dlls:
- if isSystemDLL(dll):
- others.add(dll)
- continue
- if dll not in images and dll not in dependents:
- images.add(dll)
- if uses_import_module:
- warnings.add(dll)
- return dependents, warnings, others
-
-# DLLs to be excluded
-# XXX This list is NOT complete (it cannot be)
-# Note: ALL ENTRIES MUST BE IN LOWER CASE!
-EXCLUDED_DLLS = (
- "advapi32.dll",
- "comctl32.dll",
- "comdlg32.dll",
- "crtdll.dll",
- "gdi32.dll",
- "glu32.dll",
- "opengl32.dll",
- "imm32.dll",
- "kernel32.dll",
- "mfc42.dll",
- "msvcirt.dll",
- "msvcrt.dll",
- "msvcrtd.dll",
- "ntdll.dll",
- "odbc32.dll",
- "ole32.dll",
- "oleaut32.dll",
- "rpcrt4.dll",
- "shell32.dll",
- "shlwapi.dll",
- "user32.dll",
- "version.dll",
- "winmm.dll",
- "winspool.drv",
- "ws2_32.dll",
- "ws2help.dll",
- "wsock32.dll",
- "netapi32.dll",
-
- "gdiplus.dll",
- )
-
-# XXX Perhaps it would be better to assume dlls from the systemdir are system dlls,
-# and make some exceptions for known dlls, like msvcr71, pythonXY.dll, and so on?
-def isSystemDLL(pathname):
- if os.path.basename(pathname).lower() in ("msvcr71.dll", "msvcr71d.dll"):
- return 0
- if os.path.basename(pathname).lower() in EXCLUDED_DLLS:
- return 1
- # How can we determine whether a dll is a 'SYSTEM DLL'?
- # Is it sufficient to use the Image Load Address?
- import struct
- file = open(pathname, "rb")
- if file.read(2) != "MZ":
- raise Exception, "Seems not to be an exe-file"
- file.seek(0x3C)
- pe_ofs = struct.unpack("i", file.read(4))[0]
- file.seek(pe_ofs)
- if file.read(4) != "PE\000\000":
- raise Exception, ("Seems not to be an exe-file", pathname)
- file.read(20 + 28) # COFF File Header, offset of ImageBase in Optional Header
- imagebase = struct.unpack("I", file.read(4))[0]
- return not (imagebase < 0x70000000)
-
-def byte_compile(py_files, optimize=0, force=0,
- target_dir=None, verbose=1, dry_run=0,
- direct=None):
-
- if direct is None:
- direct = (__debug__ and optimize == 0)
-
- # "Indirect" byte-compilation: write a temporary script and then
- # run it with the appropriate flags.
- if not direct:
- from tempfile import mktemp
- from distutils.util import execute
- script_name = mktemp(".py")
- if verbose:
- print "writing byte-compilation script '%s'" % script_name
- if not dry_run:
- script = open(script_name, "w")
- script.write("""\
-from py2exe.build_exe import byte_compile
-from modulefinder import Module
-files = [
-""")
-
- for f in py_files:
- script.write("Module(%s, %s, %s),\n" % \
- (`f.__name__`, `f.__file__`, `f.__path__`))
- script.write("]\n")
- script.write("""
-byte_compile(files, optimize=%s, force=%s,
- target_dir=%s,
- verbose=%s, dry_run=0,
- direct=1)
-""" % (`optimize`, `force`, `target_dir`, `verbose`))
-
- script.close()
-
- cmd = [sys.executable, script_name]
- if optimize == 1:
- cmd.insert(1, "-O")
- elif optimize == 2:
- cmd.insert(1, "-OO")
- spawn(cmd, verbose=verbose, dry_run=dry_run)
- execute(os.remove, (script_name,), "removing %s" % script_name,
- verbose=verbose, dry_run=dry_run)
-
-
- else:
- from py_compile import compile
- from distutils.dir_util import mkpath
- from distutils.dep_util import newer
- from distutils.file_util import copy_file
-
- for file in py_files:
- # Terminology from the py_compile module:
- # cfile - byte-compiled file
- # dfile - purported source filename (same as 'file' by default)
- cfile = file.__name__.replace('.', '\\')
-
- if file.__path__:
- dfile = cfile + '\\__init__.py' + (__debug__ and 'c' or 'o')
- else:
- dfile = cfile + '.py' + (__debug__ and 'c' or 'o')
- if target_dir:
- cfile = os.path.join(target_dir, dfile)
-
- if force or newer(file.__file__, cfile):
- if verbose:
- print "byte-compiling %s to %s" % (file.__file__, dfile)
- if not dry_run:
- mkpath(os.path.dirname(cfile))
- suffix = os.path.splitext(file.__file__)[1]
- if suffix in (".py", ".pyw"):
- compile(file.__file__, cfile, dfile)
- elif suffix in _py_suffixes:
- # Minor problem: This will happily copy a file
- # .pyo to .pyc or .pyc to
- # .pyo, but it does seem to work.
- copy_file(file.__file__, cfile, preserve_mode=0)
- else:
- raise RuntimeError \
- ("Don't know how to handle %r" % file.__file__)
- else:
- if verbose:
- print "skipping byte-compilation of %s to %s" % \
- (file.__file__, dfile)
- compiled_files = []
- for file in py_files:
- cfile = file.__name__.replace('.', '\\')
-
- if file.__path__:
- dfile = cfile + '\\__init__.py' + (optimize and 'o' or 'c')
- else:
- dfile = cfile + '.py' + (optimize and 'o' or 'c')
- compiled_files.append(dfile)
- return compiled_files
-
-# byte_compile()
-
-# win32com makepy helper.
-def collect_win32com_genpy(path, typelibs, verbose=0, dry_run=0):
- import win32com
- from win32com.client import gencache, makepy
- from distutils.file_util import copy_file
-
- old_gen_path = win32com.__gen_path__
- num = 0
- try:
- win32com.__gen_path__ = path
- win32com.gen_py.__path__ = [path]
- gencache.__init__()
- for info in typelibs:
- guid, lcid, major, minor = info[:4]
- # They may provide an input filename in the tuple - in which case
- # they will have pre-generated it on a machine with the typelibs
- # installed, and just want us to include it.
- fname_in = None
- if len(info) > 4:
- fname_in = info[4]
- if fname_in is not None:
- base = gencache.GetGeneratedFileName(guid, lcid, major, minor)
- fname_out = os.path.join(path, base) + ".py"
- copy_file(fname_in, fname_out, verbose=verbose, dry_run=dry_run)
- num += 1
- # That's all we gotta do!
- continue
-
- # It seems bForDemand=True generates code which is missing
- # at least sometimes an import of DispatchBaseClass.
- # Until this is resolved, set it to false.
- # What's the purpose of bForDemand=True? Thomas
- # bForDemand is supposed to only generate stubs when each
- # individual object is referenced. A side-effect of that is
- # that each object gets its own source file. The intent of
- # this code was to set bForDemand=True, meaning we get the
- # 'file per object' behaviour, but then explicitly walk all
- # children forcing them to be built - so the entire object model
- # is included, but not in a huge .pyc.
- # I'm not sure why its not working :) I'll debug later.
- # bForDemand=False isn't really important here - the overhead for
- # monolithic typelib stubs is in the compilation, not the loading
- # of an existing .pyc. Mark.
-## makepy.GenerateFromTypeLibSpec(info, bForDemand = True)
- tlb_info = (guid, lcid, major, minor)
- makepy.GenerateFromTypeLibSpec(tlb_info, bForDemand = False)
- # Now get the module, and build all sub-modules.
- mod = gencache.GetModuleForTypelib(*tlb_info)
- for clsid, name in mod.CLSIDToPackageMap.items():
- try:
- gencache.GetModuleForCLSID(clsid)
- num += 1
- #print "", name
- except ImportError:
- pass
- return num
- finally:
- # restore win32com, just in case.
- win32com.__gen_path__ = old_gen_path
- win32com.gen_py.__path__ = [old_gen_path]
- gencache.__init__()
-
-# utilities hacked from distutils.dir_util
-
-def _chmod(file):
- os.chmod(file, 0777)
-
-# Helper for force_remove_tree()
-def _build_cmdtuple(path, cmdtuples):
- for f in os.listdir(path):
- real_f = os.path.join(path,f)
- if os.path.isdir(real_f) and not os.path.islink(real_f):
- _build_cmdtuple(real_f, cmdtuples)
- else:
- cmdtuples.append((_chmod, real_f))
- cmdtuples.append((os.remove, real_f))
- cmdtuples.append((os.rmdir, path))
-
-def force_remove_tree (directory, verbose=0, dry_run=0):
- """Recursively remove an entire directory tree. Any errors are ignored
- (apart from being reported to stdout if 'verbose' is true).
- """
- import distutils
- from distutils.util import grok_environment_error
- _path_created = distutils.dir_util._path_created
-
- if verbose:
- print "removing '%s' (and everything under it)" % directory
- if dry_run:
- return
- cmdtuples = []
- _build_cmdtuple(directory, cmdtuples)
- for cmd in cmdtuples:
- try:
- cmd[0](cmd[1])
- # remove dir from cache if it's already there
- abspath = os.path.abspath(cmd[1])
- if _path_created.has_key(abspath):
- del _path_created[abspath]
- except (IOError, OSError), exc:
- if verbose:
- print grok_environment_error(
- exc, "error removing %s: " % directory)
+# Changes:
+#
+# can now specify 'zipfile = None', in this case the Python module
+# library archive is appended to the exe.
+
+# Todo:
+#
+# Make 'unbuffered' a per-target option
+
+from distutils.core import Command
+from distutils.spawn import spawn
+from distutils.errors import *
+import sys
+import os
+import imp
+import types
+import stat
+import marshal
+import zipfile
+import sets
+import tempfile
+import struct
+import re
+
+is_win64 = struct.calcsize("P") == 8
+
+
+def _is_debug_build():
+ for ext, _, _ in imp.get_suffixes():
+ if ext == "_d.pyd":
+ return True
+ return False
+
+
+is_debug_build = _is_debug_build()
+
+if is_debug_build:
+ python_dll = "python%d%d_d.dll" % sys.version_info[:2]
+else:
+ python_dll = "python%d%d.dll" % sys.version_info[:2]
+
+# resource constants
+RT_BITMAP = 2
+RT_MANIFEST = 24
+
+# Pattern for modifying the 'requestedExecutionLevel' in the manifest. Groups
+# are setup so all text *except* for the values is matched.
+pat_manifest_uac = re.compile(
+ r'(^.*", path
+ mod = imp.load_dynamic(__name__, path)
+## mod.frozen = 1
+__load()
+del __load
+"""
+
+# A very loosely defined "target". We assume either a "script" or "modules"
+# attribute. Some attributes will be target specific.
+
+
+class Target:
+ # A custom requestedExecutionLevel for the User Access Control portion
+ # of the manifest for the target. May be a string, which will be used for
+ # the 'requestedExecutionLevel' portion and False for 'uiAccess', or a tuple
+ # of (string, bool) which specifies both values. If specified and the
+ # target's 'template' executable has no manifest (ie, python 2.5 and
+ # earlier), then a default manifest is created, otherwise the manifest from
+ # the template is copied then updated.
+ uac_info = None
+
+ def __init__(self, **kw):
+ self.__dict__.update(kw)
+ # If modules is a simple string, assume they meant list
+ m = self.__dict__.get("modules")
+ if m and type(m) in (str,):
+ self.modules = [m]
+
+ def get_dest_base(self):
+ dest_base = getattr(self, "dest_base", None)
+ if dest_base:
+ return dest_base
+ script = getattr(self, "script", None)
+ if script:
+ return os.path.basename(os.path.splitext(script)[0])
+ modules = getattr(self, "modules", None)
+ assert modules, "no script, modules or dest_base specified"
+ return modules[0].split(".")[-1]
+
+ def validate(self):
+ resources = getattr(self, "bitmap_resources", []) + \
+ getattr(self, "icon_resources", [])
+ for r_id, r_filename in resources:
+ if type(r_id) != type(0):
+ raise DistutilsOptionError("Resource ID must be an integer")
+ if not os.path.isfile(r_filename):
+ raise DistutilsOptionError(
+ "Resource filename '%s' does not exist" % r_filename)
+
+
+def FixupTargets(targets, default_attribute):
+ if not targets:
+ return targets
+ ret = []
+ for target_def in targets:
+ if type(target_def) in (str,):
+ # Create a default target object, with the string as the attribute
+ target = Target(**{default_attribute: target_def})
+ else:
+ d = getattr(target_def, "__dict__", target_def)
+ if default_attribute not in d:
+ raise DistutilsOptionError(
+ "This target class requires an attribute '%s'" % default_attribute)
+ target = Target(**d)
+ target.validate()
+ ret.append(target)
+ return ret
+
+
+class py2exe(Command):
+ description = ""
+ # List of option tuples: long name, short name (None if no short
+ # name), and help string.
+ user_options = [
+ ('optimize=', 'O',
+ "optimization level: -O1 for \"python -O\", "
+ "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
+ ('dist-dir=', 'd',
+ "directory to put final built distributions in (default is dist)"),
+
+ ("excludes=", 'e',
+ "comma-separated list of modules to exclude"),
+ ("dll-excludes=", None,
+ "comma-separated list of DLLs to exclude"),
+ ("ignores=", None,
+ "comma-separated list of modules to ignore if they are not found"),
+ ("includes=", 'i',
+ "comma-separated list of modules to include"),
+ ("packages=", 'p',
+ "comma-separated list of packages to include"),
+ ("skip-scan=", None,
+ "comma-separated list of modules not to scan for imported modules"),
+
+ ("compressed", 'c',
+ "create a compressed zipfile"),
+
+ ("xref", 'x',
+ "create and show a module cross reference"),
+
+ ("bundle-files=", 'b',
+ "bundle dlls in the zipfile or the exe. Valid levels are 1, 2, or 3 (default)"),
+
+ ("skip-archive", None,
+ "do not place Python bytecode files in an archive, put them directly in the file system"),
+
+ ("ascii", 'a',
+ "do not automatically include encodings and codecs"),
+
+ ('custom-boot-script=', None,
+ "Python file that will be run when setting up the runtime environment"),
+ ]
+
+ boolean_options = ["compressed", "xref", "ascii", "skip-archive"]
+
+ def initialize_options(self):
+ self.xref = 0
+ self.compressed = 0
+ self.unbuffered = 0
+ self.optimize = 0
+ self.includes = None
+ self.excludes = None
+ self.skip_scan = None
+ self.ignores = None
+ self.packages = None
+ self.dist_dir = None
+ self.dll_excludes = None
+ self.typelibs = None
+ self.bundle_files = 3
+ self.skip_archive = 0
+ self.ascii = 0
+ self.custom_boot_script = None
+
+ def finalize_options(self):
+ self.optimize = int(self.optimize)
+ self.excludes = fancy_split(self.excludes)
+ self.includes = fancy_split(self.includes)
+ self.skip_scan = fancy_split(self.skip_scan)
+ self.ignores = fancy_split(self.ignores)
+ self.bundle_files = int(self.bundle_files)
+ if self.bundle_files < 1 or self.bundle_files > 3:
+ raise DistutilsOptionError(
+ "bundle-files must be 1, 2, or 3, not %s" % self.bundle_files)
+ if is_win64 and self.bundle_files < 3:
+ raise DistutilsOptionError(
+ "bundle-files %d not yet supported on win64" % self.bundle_files)
+ if self.skip_archive:
+ if self.compressed:
+ raise DistutilsOptionError(
+ "can't compress when skipping archive")
+ if self.distribution.zipfile is None:
+ raise DistutilsOptionError(
+ "zipfile cannot be None when skipping archive")
+ # includes is stronger than excludes
+ for m in self.includes:
+ if m in self.excludes:
+ self.excludes.remove(m)
+ self.packages = fancy_split(self.packages)
+ self.set_undefined_options('bdist',
+ ('dist_dir', 'dist_dir'))
+ self.dll_excludes = [x.lower() for x in fancy_split(self.dll_excludes)]
+
+ def run(self):
+ build = self.reinitialize_command('build')
+ build.run()
+ sys_old_path = sys.path[:]
+ if build.build_platlib is not None:
+ sys.path.insert(0, build.build_platlib)
+ if build.build_lib is not None:
+ sys.path.insert(0, build.build_lib)
+ try:
+ self._run()
+ finally:
+ sys.path = sys_old_path
+
+ def _run(self):
+ self.create_directories()
+ self.plat_prepare()
+ self.fixup_distribution()
+
+ dist = self.distribution
+
+ # all of these contain module names
+ required_modules = []
+ for target in dist.com_server + dist.service + dist.ctypes_com_server:
+ required_modules.extend(target.modules)
+ # and these contains file names
+ required_files = [target.script
+ for target in dist.windows + dist.console]
+
+ mf = self.create_modulefinder()
+
+ # These are the name of a script, but used as a module!
+ for f in dist.isapi:
+ mf.load_file(f.script)
+
+ if self.typelibs:
+ print("*** generate typelib stubs ***")
+ from distutils.dir_util import mkpath
+ genpy_temp = os.path.join(self.temp_dir, "win32com", "gen_py")
+ mkpath(genpy_temp)
+ num_stubs = collect_win32com_genpy(genpy_temp,
+ self.typelibs,
+ verbose=self.verbose,
+ dry_run=self.dry_run)
+ print(("collected %d stubs from %d type libraries"
+ % (num_stubs, len(self.typelibs))))
+ mf.load_package("win32com.gen_py", genpy_temp)
+ self.packages.append("win32com.gen_py")
+
+ # monkey patching the compile builtin.
+ # The idea is to include the filename in the error message
+ orig_compile = compile
+ import builtins
+
+ def my_compile(source, filename, *args):
+ try:
+ result = orig_compile(source, filename, *args)
+ except Exception as details:
+ raise DistutilsError("compiling '%s' failed\n %s: %s" %
+ (filename, details.__class__.__name__, details))
+ return result
+ builtins.compile = my_compile
+
+ print("*** searching for required modules ***")
+ self.find_needed_modules(mf, required_files, required_modules)
+
+ print("*** parsing results ***")
+ py_files, extensions, builtins = self.parse_mf_results(mf)
+
+ if self.xref:
+ mf.create_xref()
+
+ print("*** finding dlls needed ***")
+ dlls = self.find_dlls(extensions)
+ self.plat_finalize(mf.modules, py_files, extensions, dlls)
+ dlls = [item for item in dlls
+ if os.path.basename(item).lower() not in self.dll_excludes]
+ # should we filter self.other_depends in the same way?
+
+ print("*** create binaries ***")
+ self.create_binaries(py_files, extensions, dlls)
+
+ self.fix_badmodules(mf)
+
+ if mf.any_missing():
+ print("The following modules appear to be missing")
+ print((mf.any_missing()))
+
+ if self.other_depends:
+ print()
+ print("*** binary dependencies ***")
+ print("Your executable(s) also depend on these dlls which are not included,")
+ print("you may or may not need to distribute them.")
+ print()
+ print("Make sure you have the license if you distribute any of them, and")
+ print(
+ "make sure you don't distribute files belonging to the operating system.")
+ print()
+ for fnm in self.other_depends:
+ print((" ", os.path.basename(fnm), "-", fnm.strip()))
+
+ def create_modulefinder(self):
+ from modulefinder import ReplacePackage
+ from py2exe.mf import ModuleFinder
+ ReplacePackage("_xmlplus", "xml")
+ return ModuleFinder(excludes=self.excludes, skip_scan=self.skip_scan)
+
+ def fix_badmodules(self, mf):
+ # This dictionary maps additional builtin module names to the
+ # module that creates them.
+ # For example, 'wxPython.misc' creates a builtin module named
+ # 'miscc'.
+ builtins = {"clip_dndc": "wxPython.clip_dnd",
+ "cmndlgsc": "wxPython.cmndlgs",
+ "controls2c": "wxPython.controls2",
+ "controlsc": "wxPython.controls",
+ "eventsc": "wxPython.events",
+ "filesysc": "wxPython.filesys",
+ "fontsc": "wxPython.fonts",
+ "framesc": "wxPython.frames",
+ "gdic": "wxPython.gdi",
+ "imagec": "wxPython.image",
+ "mdic": "wxPython.mdi",
+ "misc2c": "wxPython.misc2",
+ "miscc": "wxPython.misc",
+ "printfwc": "wxPython.printfw",
+ "sizersc": "wxPython.sizers",
+ "stattoolc": "wxPython.stattool",
+ "streamsc": "wxPython.streams",
+ "utilsc": "wxPython.utils",
+ "windows2c": "wxPython.windows2",
+ "windows3c": "wxPython.windows3",
+ "windowsc": "wxPython.windows",
+ }
+
+ # Somewhat hackish: change modulefinder's badmodules dictionary in place.
+ bad = mf.badmodules
+ # mf.badmodules is a dictionary mapping unfound module names
+ # to another dictionary, the keys of this are the module names
+ # importing the unknown module. For the 'miscc' module
+ # mentioned above, it looks like this:
+ # mf.badmodules["miscc"] = { "wxPython.miscc": 1 }
+ for name in mf.any_missing():
+ if name in self.ignores:
+ del bad[name]
+ continue
+ mod = builtins.get(name, None)
+ if mod is not None:
+ if mod in bad[name] and bad[name] == {mod: 1}:
+ del bad[name]
+
+ def find_dlls(self, extensions):
+ dlls = [item.__file__ for item in extensions]
+# extra_path = ["."] # XXX
+ extra_path = []
+ dlls, unfriendly_dlls, other_depends = \
+ self.find_dependend_dlls(dlls,
+ extra_path + sys.path,
+ self.dll_excludes)
+ self.other_depends = other_depends
+ # dlls contains the path names of all dlls we need.
+ # If a dll uses a function PyImport_ImportModule (or what was it?),
+ # it's name is additionally in unfriendly_dlls.
+ for item in extensions:
+ if item.__file__ in dlls:
+ dlls.remove(item.__file__)
+ return dlls
+
+ def create_directories(self):
+ bdist_base = self.get_finalized_command('bdist').bdist_base
+ self.bdist_dir = os.path.join(bdist_base, 'winexe')
+
+ collect_name = "collect-%d.%d" % sys.version_info[:2]
+ self.collect_dir = os.path.abspath(
+ os.path.join(self.bdist_dir, collect_name))
+ self.mkpath(self.collect_dir)
+
+ bundle_name = "bundle-%d.%d" % sys.version_info[:2]
+ self.bundle_dir = os.path.abspath(
+ os.path.join(self.bdist_dir, bundle_name))
+ self.mkpath(self.bundle_dir)
+
+ self.temp_dir = os.path.abspath(os.path.join(self.bdist_dir, "temp"))
+ self.mkpath(self.temp_dir)
+
+ self.dist_dir = os.path.abspath(self.dist_dir)
+ self.mkpath(self.dist_dir)
+
+ if self.distribution.zipfile is None:
+ self.lib_dir = self.dist_dir
+ else:
+ self.lib_dir = os.path.join(self.dist_dir,
+ os.path.dirname(self.distribution.zipfile))
+ self.mkpath(self.lib_dir)
+
+ def copy_extensions(self, extensions):
+ print("*** copy extensions ***")
+ # copy the extensions to the target directory
+ for item in extensions:
+ src = item.__file__
+ if self.bundle_files > 2: # don't bundle pyds and dlls
+ dst = os.path.join(self.lib_dir, (item.__pydfile__))
+ self.copy_file(src, dst, preserve_mode=0)
+ self.lib_files.append(dst)
+ else:
+ # we have to preserve the packages
+ package = "\\".join(item.__name__.split(".")[:-1])
+ if package:
+ dst = os.path.join(package, os.path.basename(src))
+ else:
+ dst = os.path.basename(src)
+ self.copy_file(src, os.path.join(
+ self.collect_dir, dst), preserve_mode=0)
+ self.compiled_files.append(dst)
+
+ def copy_dlls(self, dlls):
+ # copy needed dlls where they belong.
+ print("*** copy dlls ***")
+ if self.bundle_files < 3:
+ self.copy_dlls_bundle_files(dlls)
+ return
+ # dlls belong into the lib_dir, except those listed in dlls_in_exedir,
+ # which have to go into exe_dir (pythonxy.dll, w9xpopen.exe).
+ for dll in dlls:
+ base = os.path.basename(dll)
+ if base.lower() in self.dlls_in_exedir:
+ # These special dlls cannot be in the lib directory,
+ # they must go into the exe directory.
+ dst = os.path.join(self.exe_dir, base)
+ else:
+ dst = os.path.join(self.lib_dir, base)
+ _, copied = self.copy_file(dll, dst, preserve_mode=0)
+ if not self.dry_run and copied and base.lower() == python_dll.lower():
+ # If we actually copied pythonxy.dll, we have to patch it.
+ #
+ # Previously, the code did it every time, but this
+ # breaks if, for example, someone runs UPX over the
+ # dist directory. Patching an UPX'd dll seems to work
+ # (no error is detected when patching), but the
+ # resulting dll does not work anymore.
+ #
+ # The function restores the file times so
+ # dependencies still work correctly.
+ self.patch_python_dll_winver(dst)
+
+ self.lib_files.append(dst)
+
+ def copy_dlls_bundle_files(self, dlls):
+ # If dlls have to be bundled, they are copied into the
+ # collect_dir and will be added to the list of files to
+ # include in the zip archive 'self.compiled_files'.
+ #
+ # dlls listed in dlls_in_exedir have to be treated differently:
+ #
+ for dll in dlls:
+ base = os.path.basename(dll)
+ if base.lower() in self.dlls_in_exedir:
+ # pythonXY.dll must be bundled as resource.
+ # w9xpopen.exe must be copied to self.exe_dir.
+ if base.lower() == python_dll.lower() and self.bundle_files < 2:
+ dst = os.path.join(self.bundle_dir, base)
+ else:
+ dst = os.path.join(self.exe_dir, base)
+ _, copied = self.copy_file(dll, dst, preserve_mode=0)
+ if not self.dry_run and copied and base.lower() == python_dll.lower():
+ # If we actually copied pythonxy.dll, we have to
+ # patch it. Well, since it's impossible to load
+ # resources from the bundled dlls it probably
+ # doesn't matter.
+ self.patch_python_dll_winver(dst)
+ self.lib_files.append(dst)
+ continue
+
+ dst = os.path.join(self.collect_dir, os.path.basename(dll))
+ self.copy_file(dll, dst, preserve_mode=0)
+ # Make sure they will be included into the zipfile.
+ self.compiled_files.append(os.path.basename(dst))
+
+ def create_binaries(self, py_files, extensions, dlls):
+ dist = self.distribution
+
+ # byte compile the python modules into the target directory
+ print("*** byte compile python files ***")
+ self.compiled_files = byte_compile(py_files,
+ target_dir=self.collect_dir,
+ optimize=self.optimize,
+ force=0,
+ verbose=self.verbose,
+ dry_run=self.dry_run)
+
+ self.lib_files = []
+ self.console_exe_files = []
+ self.windows_exe_files = []
+ self.service_exe_files = []
+ self.comserver_files = []
+
+ self.copy_extensions(extensions)
+ self.copy_dlls(dlls)
+
+ # create the shared zipfile containing all Python modules
+ if dist.zipfile is None:
+ fd, archive_name = tempfile.mkstemp()
+ os.close(fd)
+ else:
+ archive_name = os.path.join(self.lib_dir,
+ os.path.basename(dist.zipfile))
+
+ arcname = self.make_lib_archive(archive_name,
+ base_dir=self.collect_dir,
+ files=self.compiled_files,
+ verbose=self.verbose,
+ dry_run=self.dry_run)
+ if dist.zipfile is not None:
+ self.lib_files.append(arcname)
+
+ for target in self.distribution.isapi:
+ print("*** copy isapi support DLL ***")
+ # Locate the support DLL, and copy as "_script.dll", just like
+ # isapi itself
+ import isapi
+ src_name = is_debug_build and "PyISAPI_loader_d.dll" or \
+ "PyISAPI_loader.dll"
+ src = os.path.join(isapi.__path__[0], src_name)
+ # destination name is "_{module_name}.dll" just like pyisapi does.
+ script_base = os.path.splitext(os.path.basename(target.script))[0]
+ dst = os.path.join(self.exe_dir, "_" + script_base + ".dll")
+ self.copy_file(src, dst, preserve_mode=0)
+
+ if self.distribution.has_data_files():
+ print("*** copy data files ***")
+ install_data = self.reinitialize_command('install_data')
+ install_data.install_dir = self.dist_dir
+ install_data.ensure_finalized()
+ install_data.run()
+
+ self.lib_files.extend(install_data.get_outputs())
+
+ # build the executables
+ for target in dist.console:
+ dst = self.build_executable(target, self.get_console_template(),
+ arcname, target.script)
+ self.console_exe_files.append(dst)
+ for target in dist.windows:
+ dst = self.build_executable(target, self.get_windows_template(),
+ arcname, target.script)
+ self.windows_exe_files.append(dst)
+ for target in dist.service:
+ dst = self.build_service(target, self.get_service_template(),
+ arcname)
+ self.service_exe_files.append(dst)
+
+ for target in dist.isapi:
+ dst = self.build_isapi(target, self.get_isapi_template(), arcname)
+
+ for target in dist.com_server:
+ if getattr(target, "create_exe", True):
+ dst = self.build_comserver(target, self.get_comexe_template(),
+ arcname)
+ self.comserver_files.append(dst)
+ if getattr(target, "create_dll", True):
+ dst = self.build_comserver(target, self.get_comdll_template(),
+ arcname)
+ self.comserver_files.append(dst)
+
+ for target in dist.ctypes_com_server:
+ dst = self.build_comserver(target, self.get_ctypes_comdll_template(),
+ arcname, boot_script="ctypes_com_server")
+ self.comserver_files.append(dst)
+
+ if dist.zipfile is None:
+ os.unlink(arcname)
+ else:
+ if self.bundle_files < 3 or self.compressed:
+ arcbytes = open(arcname, "rb").read()
+ arcfile = open(arcname, "wb")
+
+ if self.bundle_files < 2: # bundle pythonxy.dll also
+ print(("Adding %s to %s" % (python_dll, arcname)))
+ arcfile.write("")
+ bytes = open(os.path.join(
+ self.bundle_dir, python_dll), "rb").read()
+ arcfile.write(struct.pack("i", len(bytes)))
+ arcfile.write(bytes) # python dll
+
+ if self.compressed:
+ # prepend zlib.pyd also
+ zlib_file = imp.find_module("zlib")[0]
+ if zlib_file:
+ print(("Adding zlib%s.pyd to %s" %
+ (is_debug_build and "_d" or "", arcname)))
+ arcfile.write("")
+ bytes = zlib_file.read()
+ arcfile.write(struct.pack("i", len(bytes)))
+ arcfile.write(bytes) # zlib.pyd
+
+ arcfile.write(arcbytes)
+
+# if self.bundle_files < 2:
+# remove python dll from the exe_dir, since it is now bundled.
+#### os.remove(os.path.join(self.exe_dir, python_dll))
+
+ # for user convenience, let subclasses override the templates to use
+
+ def get_console_template(self):
+ return is_debug_build and "run_d.exe" or "run.exe"
+
+ def get_windows_template(self):
+ return is_debug_build and "run_w_d.exe" or "run_w.exe"
+
+ def get_service_template(self):
+ return is_debug_build and "run_d.exe" or "run.exe"
+
+ def get_isapi_template(self):
+ return is_debug_build and "run_isapi_d.dll" or "run_isapi.dll"
+
+ def get_comexe_template(self):
+ return is_debug_build and "run_w_d.exe" or "run_w.exe"
+
+ def get_comdll_template(self):
+ return is_debug_build and "run_dll_d.dll" or "run_dll.dll"
+
+ def get_ctypes_comdll_template(self):
+ return is_debug_build and "run_ctypes_dll_d.dll" or "run_ctypes_dll.dll"
+
+ def fixup_distribution(self):
+ dist = self.distribution
+
+ # Convert our args into target objects.
+ dist.com_server = FixupTargets(dist.com_server, "modules")
+ dist.ctypes_com_server = FixupTargets(
+ dist.ctypes_com_server, "modules")
+ dist.service = FixupTargets(dist.service, "modules")
+ dist.windows = FixupTargets(dist.windows, "script")
+ dist.console = FixupTargets(dist.console, "script")
+ dist.isapi = FixupTargets(dist.isapi, "script")
+
+ # make sure all targets use the same directory, this is
+ # also the directory where the pythonXX.dll must reside
+ paths = sets.Set()
+ for target in dist.com_server + dist.service \
+ + dist.windows + dist.console + dist.isapi:
+ paths.add(os.path.dirname(target.get_dest_base()))
+
+ if len(paths) > 1:
+ raise DistutilsOptionError("all targets must use the same directory: %s" %
+ [p for p in paths])
+ if paths:
+ exe_dir = paths.pop() # the only element
+ if os.path.isabs(exe_dir):
+ raise DistutilsOptionError(
+ "exe directory must be relative: %s" % exe_dir)
+ self.exe_dir = os.path.join(self.dist_dir, exe_dir)
+ self.mkpath(self.exe_dir)
+ else:
+ # Do we allow to specify no targets?
+ # We can at least build a zipfile...
+ self.exe_dir = self.lib_dir
+
+ def get_boot_script(self, boot_type):
+ # return the filename of the script to use for com servers.
+ thisfile = sys.modules['py2exe.build_exe'].__file__
+ return os.path.join(os.path.dirname(thisfile),
+ "boot_" + boot_type + ".py")
+
+ def build_comserver(self, target, template, arcname, boot_script="com_servers"):
+ # Build a dll and an exe executable hosting all the com
+ # objects listed in module_names.
+ # The basename of the dll/exe is the last part of the first module.
+ # Do we need a way to specify the name of the files to be built?
+
+ # Setup the variables our boot script needs.
+ vars = {"com_module_names": target.modules}
+ boot = self.get_boot_script(boot_script)
+ # and build it
+ return self.build_executable(target, template, arcname, boot, vars)
+
+ def get_service_names(self, module_name):
+ # import the script with every side effect :)
+ __import__(module_name)
+ mod = sys.modules[module_name]
+ for name, klass in list(mod.__dict__.items()):
+ if hasattr(klass, "_svc_name_"):
+ break
+ else:
+ raise RuntimeError("No services in module")
+ deps = ()
+ if hasattr(klass, "_svc_deps_"):
+ deps = klass._svc_deps_
+ return klass.__name__, klass._svc_name_, klass._svc_display_name_, deps
+
+ def build_service(self, target, template, arcname):
+ # It should be possible to host many modules in a single service -
+ # but this is yet to be tested.
+ assert len(target.modules) == 1, "We only support one service module"
+
+ cmdline_style = getattr(target, "cmdline_style", "py2exe")
+ if cmdline_style not in ["py2exe", "pywin32", "custom"]:
+ raise RuntimeError("cmdline_handler invalid")
+
+ vars = {"service_module_names": target.modules,
+ "cmdline_style": cmdline_style,
+ }
+ boot = self.get_boot_script("service")
+ return self.build_executable(target, template, arcname, boot, vars)
+
+ def build_isapi(self, target, template, arcname):
+ target_module = os.path.splitext(os.path.basename(target.script))[0]
+ vars = {"isapi_module_name": target_module,
+ }
+ return self.build_executable(target, template, arcname, None, vars)
+
+ def build_manifest(self, target, template):
+ # Optionally return a manifest to be included in the target executable.
+ # Note for Python 2.6 and later, its *necessary* to include a manifest
+ # which correctly references the CRT. For earlier versions, a manifest
+ # is optional, and only necessary to customize things like
+ # Vista's User Access Control 'requestedExecutionLevel' setting, etc.
+ default_manifest = """
+
+
+
+
+
+
+
+
+
+ """
+ from py2exe_util import load_resource
+ if os.path.splitext(template)[1] == ".exe":
+ rid = 1
+ else:
+ rid = 2
+ try:
+ # Manfiests have resource type of 24, and ID of either 1 or 2.
+ mfest = load_resource(ensure_unicode(template), RT_MANIFEST, rid)
+ # we consider the manifest 'changed' as we know we clobber all
+ # resources including the existing manifest - so this manifest must
+ # get written even if we make no other changes.
+ changed = True
+ except RuntimeError:
+ mfest = default_manifest
+ # in this case the template had no existing manifest, so its
+ # not considered 'changed' unless we make further changes later.
+ changed = False
+ # update the manifest according to our options.
+ # for now a regex will do.
+ if target.uac_info:
+ changed = True
+ if isinstance(target.uac_info, tuple):
+ exec_level, ui = target.uac_info
+ else:
+ exec_level = target.uac_info
+ ui = False
+ new_lines = []
+ for line in mfest.splitlines():
+ repl = r'\1%s\3%s\5' % (exec_level, ui)
+ new_lines.append(re.sub(pat_manifest_uac, repl, line))
+ mfest = "".join(new_lines)
+ if not changed:
+ return None, None
+ return mfest, rid
+
+ def build_executable(self, target, template, arcname, script, vars={}):
+ # Build an executable for the target
+ # template is the exe-stub to use, and arcname is the zipfile
+ # containing the python modules.
+ from py2exe_util import add_resource, add_icon
+ ext = os.path.splitext(template)[1]
+ exe_base = target.get_dest_base()
+ exe_path = os.path.join(self.dist_dir, exe_base + ext)
+ # The user may specify a sub-directory for the exe - that's fine, we
+ # just specify the parent directory for the .zip
+ parent_levels = len(os.path.normpath(exe_base).split(os.sep))-1
+ lib_leaf = self.lib_dir[len(self.dist_dir)+1:]
+ relative_arcname = ((".." + os.sep) * parent_levels)
+ if lib_leaf:
+ relative_arcname += lib_leaf + os.sep
+ relative_arcname += os.path.basename(arcname)
+
+ src = os.path.join(os.path.dirname(__file__), template)
+ # We want to force the creation of this file, as otherwise distutils
+ # will see the earlier time of our 'template' file versus the later
+ # time of our modified template file, and consider our old file OK.
+ old_force = self.force
+ self.force = True
+ self.copy_file(src, exe_path, preserve_mode=0)
+ self.force = old_force
+
+ # Make sure the file is writeable...
+ os.chmod(exe_path, stat.S_IREAD | stat.S_IWRITE)
+ try:
+ f = open(exe_path, "a+b")
+ f.close()
+ except IOError as why:
+ print(("WARNING: File %s could not be opened - %s" % (exe_path, why)))
+
+ # We create a list of code objects, and write it as a marshaled
+ # stream. The framework code then just exec's these in order.
+ # First is our common boot script.
+ boot = self.get_boot_script("common")
+ boot_code = compile(file(boot, "U").read(),
+ os.path.abspath(boot), "exec")
+ code_objects = [boot_code]
+ if self.bundle_files < 3:
+ code_objects.append(
+ compile("import zipextimporter; zipextimporter.install()",
+ "", "exec"))
+ for var_name, var_val in list(vars.items()):
+ code_objects.append(
+ compile("%s=%r\n" % (var_name, var_val), var_name, "exec")
+ )
+ if self.custom_boot_script:
+ code_object = compile(file(self.custom_boot_script, "U").read() + "\n",
+ os.path.abspath(self.custom_boot_script), "exec")
+ code_objects.append(code_object)
+ if script:
+ code_object = compile(open(script, "U").read() + "\n",
+ os.path.basename(script), "exec")
+ code_objects.append(code_object)
+ code_bytes = marshal.dumps(code_objects)
+
+ if self.distribution.zipfile is None:
+ relative_arcname = ""
+
+ si = struct.pack("iiii",
+ 0x78563412, # a magic value,
+ self.optimize,
+ self.unbuffered,
+ len(code_bytes),
+ ) + relative_arcname + "\000"
+
+ script_bytes = si + code_bytes + '\000\000'
+ self.announce("add script resource, %d bytes" % len(script_bytes))
+ if not self.dry_run:
+ add_resource(ensure_unicode(exe_path),
+ script_bytes, "PYTHONSCRIPT", 1, True)
+
+ # add the pythondll as resource, and delete in self.exe_dir
+ if self.bundle_files < 2 and self.distribution.zipfile is None:
+ # bundle pythonxy.dll
+ dll_path = os.path.join(self.bundle_dir, python_dll)
+ bytes = open(dll_path, "rb").read()
+ # image, bytes, lpName, lpType
+
+ print(("Adding %s as resource to %s" % (python_dll, exe_path)))
+ add_resource(ensure_unicode(exe_path), bytes,
+ # for some reason, the 3. argument MUST BE UPPER CASE,
+ # otherwise the resource will not be found.
+ ensure_unicode(python_dll).upper(), 1, False)
+
+ if self.compressed and self.bundle_files < 3 and self.distribution.zipfile is None:
+ zlib_file = imp.find_module("zlib")[0]
+ if zlib_file:
+ print(("Adding zlib.pyd as resource to %s" % exe_path))
+ zlib_bytes = zlib_file.read()
+ add_resource(ensure_unicode(exe_path), zlib_bytes,
+ # for some reason, the 3. argument MUST BE UPPER CASE,
+ # otherwise the resource will not be found.
+ "ZLIB.PYD", 1, False)
+
+ # Handle all resources specified by the target
+ bitmap_resources = getattr(target, "bitmap_resources", [])
+ for bmp_id, bmp_filename in bitmap_resources:
+ bmp_data = open(bmp_filename, "rb").read()
+ # skip the 14 byte bitmap header.
+ if not self.dry_run:
+ add_resource(ensure_unicode(exe_path),
+ bmp_data[14:], RT_BITMAP, bmp_id, False)
+ icon_resources = getattr(target, "icon_resources", [])
+ for ico_id, ico_filename in icon_resources:
+ if not self.dry_run:
+ add_icon(ensure_unicode(exe_path),
+ ensure_unicode(ico_filename), ico_id)
+
+ # a manifest
+ mfest, mfest_id = self.build_manifest(target, src)
+ if mfest:
+ self.announce("add manifest, %d bytes" % len(mfest))
+ if not self.dry_run:
+ add_resource(ensure_unicode(exe_path), mfest,
+ RT_MANIFEST, mfest_id, False)
+
+ for res_type, res_id, data in getattr(target, "other_resources", []):
+ if not self.dry_run:
+ if isinstance(res_type, str):
+ res_type = ensure_unicode(res_type)
+ add_resource(ensure_unicode(exe_path),
+ data, res_type, res_id, False)
+
+ typelib = getattr(target, "typelib", None)
+ if typelib is not None:
+ data = open(typelib, "rb").read()
+ add_resource(ensure_unicode(exe_path), data, "TYPELIB", 1, False)
+
+ self.add_versioninfo(target, exe_path)
+
+ # Hm, this doesn't make sense with normal executables, which are
+ # already small (around 20 kB).
+ #
+ # But it would make sense with static build pythons, but not
+ # if the zipfile is appended to the exe - it will be too slow
+ # then (although it is a wonder it works at all in this case).
+ #
+ # Maybe it would be faster to use the frozen modules machanism
+ # instead of the zip-import?
+# if self.compressed:
+## import gc
+# gc.collect() # to close all open files!
+## os.system("upx -9 %s" % exe_path)
+
+ if self.distribution.zipfile is None:
+ zip_data = open(arcname, "rb").read()
+ open(exe_path, "a+b").write(zip_data)
+
+ return exe_path
+
+ def add_versioninfo(self, target, exe_path):
+ # Try to build and add a versioninfo resource
+
+ def get(name, md=self.distribution.metadata):
+ # Try to get an attribute from the target, if not defined
+ # there, from the distribution's metadata, or None. Note
+ # that only *some* attributes are allowed by distutils on
+ # the distribution's metadata: version, description, and
+ # name.
+ return getattr(target, name, getattr(md, name, None))
+
+ version = get("version")
+ if version is None:
+ return
+
+ from py2exe.resources.VersionInfo import Version, RT_VERSION, VersionError
+ version = Version(version,
+ file_description=get("description"),
+ comments=get("comments"),
+ company_name=get("company_name"),
+ legal_copyright=get("copyright"),
+ legal_trademarks=get("trademarks"),
+ original_filename=os.path.basename(exe_path),
+ product_name=get("name"),
+ product_version=get("product_version") or version)
+ try:
+ bytes = version.resource_bytes()
+ except VersionError as detail:
+ self.warn("Version Info will not be included:\n %s" % detail)
+ return
+
+ from py2exe_util import add_resource
+ add_resource(ensure_unicode(exe_path), bytes, RT_VERSION, 1, False)
+
+ def patch_python_dll_winver(self, dll_name, new_winver=None):
+ from py2exe.resources.StringTables import StringTable, RT_STRING
+ from py2exe_util import add_resource, load_resource
+ from py2exe.resources.VersionInfo import RT_VERSION
+
+ new_winver = new_winver or self.distribution.metadata.name or "py2exe"
+ if self.verbose:
+ print(("setting sys.winver for '%s' to '%s'" %
+ (dll_name, new_winver)))
+ if self.dry_run:
+ return
+
+ # We preserve the times on the file, so the dependency tracker works.
+ st = os.stat(dll_name)
+ # and as the resource functions silently fail if the open fails,
+ # check it explicitly.
+ os.chmod(dll_name, stat.S_IREAD | stat.S_IWRITE)
+ try:
+ f = open(dll_name, "a+b")
+ f.close()
+ except IOError as why:
+ print(("WARNING: File %s could not be opened - %s" % (dll_name, why)))
+ # We aren't smart enough to update string resources in place, so we need
+ # to persist other resources we care about.
+ unicode_name = ensure_unicode(dll_name)
+
+ # Preserve existing version info (all versions should have this)
+ ver_info = load_resource(unicode_name, RT_VERSION, 1)
+ # Preserve an existing manifest (only python26.dll+ will have this)
+ try:
+ # Manfiests have resource type of 24, and ID of either 1 or 2.
+ mfest = load_resource(unicode_name, RT_MANIFEST, 2)
+ except RuntimeError:
+ mfest = None
+
+ # Start putting the resources back, passing 'delete=True' for the first.
+ add_resource(unicode_name, ver_info, RT_VERSION, 1, True)
+ if mfest is not None:
+ add_resource(unicode_name, mfest, RT_MANIFEST, 2, False)
+
+ # OK - do the strings.
+ s = StringTable()
+ # 1000 is the resource ID Python loads for its winver.
+ s.add_string(1000, new_winver)
+ for id, data in s.binary():
+ add_resource(ensure_unicode(dll_name), data, RT_STRING, id, False)
+
+ # restore the time.
+ os.utime(dll_name, (st[stat.ST_ATIME], st[stat.ST_MTIME]))
+
+ def find_dependend_dlls(self, dlls, pypath, dll_excludes):
+ import py2exe_util
+ sysdir = py2exe_util.get_sysdir()
+ windir = py2exe_util.get_windir()
+ # This is the tail of the path windows uses when looking for dlls
+ # XXX On Windows NT, the SYSTEM directory is also searched
+ exedir = os.path.dirname(sys.executable)
+ syspath = os.environ['PATH']
+ loadpath = ';'.join([exedir, sysdir, windir, syspath])
+
+ # Found by Duncan Booth:
+ # It may be possible that bin_depends needs extension modules,
+ # so the loadpath must be extended by our python path.
+ loadpath = loadpath + ';' + ';'.join(pypath)
+
+ templates = sets.Set()
+ if self.distribution.console:
+ templates.add(self.get_console_template())
+ if self.distribution.windows:
+ templates.add(self.get_windows_template())
+ if self.distribution.service:
+ templates.add(self.get_service_template())
+ for target in self.distribution.com_server:
+ if getattr(target, "create_exe", True):
+ templates.add(self.get_comexe_template())
+ if getattr(target, "create_dll", True):
+ templates.add(self.get_comdll_template())
+
+ templates = [os.path.join(os.path.dirname(__file__), t)
+ for t in templates]
+
+ # We use Python.exe to track the dependencies of our run stubs ...
+ images = dlls + templates
+
+ self.announce("Resolving binary dependencies:")
+ excludes_use = dll_excludes[:]
+ # The MSVCRT modules are never found when using VS2008+
+ if sys.version_info > (2, 6):
+ excludes_use.append("msvcr90.dll")
+
+ # we add python.exe (aka sys.executable) to the list of images
+ # to scan for dependencies, but remove it later again from the
+ # results list. In this way pythonXY.dll is collected, and
+ # also the libraries it depends on.
+ alldlls, warnings, other_depends = \
+ bin_depends(loadpath, images + [sys.executable], excludes_use)
+ alldlls.remove(sys.executable)
+ for dll in alldlls:
+ self.announce(" %s" % dll)
+ # ... but we don't need the exe stubs run_xxx.exe
+ for t in templates:
+ alldlls.remove(t)
+
+ return alldlls, warnings, other_depends
+ # find_dependend_dlls()
+
+ def get_hidden_imports(self):
+ # imports done from builtin modules in C code (untrackable by py2exe)
+ return {"time": ["_strptime"],
+ # "datetime": ["time"],
+ "cPickle": ["copy_reg"],
+ "parser": ["copy_reg"],
+ "codecs": ["encodings"],
+
+ "cStringIO": ["copy_reg"],
+ "_sre": ["copy", "string", "sre"],
+ }
+
+ def parse_mf_results(self, mf):
+ for name, imports in list(self.get_hidden_imports().items()):
+ if name in list(mf.modules.keys()):
+ for mod in imports:
+ mf.import_hook(mod)
+
+ tcl_src_dir = tcl_dst_dir = None
+ if "Tkinter" in list(mf.modules.keys()):
+ import tkinter
+ import _tkinter
+ tk = _tkinter.create()
+ tcl_dir = tk.call("info", "library")
+ tcl_src_dir = os.path.split(tcl_dir)[0]
+ tcl_dst_dir = os.path.join(self.lib_dir, "tcl")
+
+ self.announce("Copying TCL files from %s..." % tcl_src_dir)
+ self.copy_tree(os.path.join(tcl_src_dir, "tcl%s" % _tkinter.TCL_VERSION),
+ os.path.join(tcl_dst_dir, "tcl%s" % _tkinter.TCL_VERSION))
+ self.copy_tree(os.path.join(tcl_src_dir, "tk%s" % _tkinter.TK_VERSION),
+ os.path.join(tcl_dst_dir, "tk%s" % _tkinter.TK_VERSION))
+ del tk, _tkinter, Tkinter
+
+ # Retrieve modules from modulefinder
+ py_files = []
+ extensions = []
+ builtins = []
+
+ for item in list(mf.modules.values()):
+ # There may be __main__ modules (from mf.run_script), but
+ # we don't need them in the zipfile we build.
+ if item.__name__ == "__main__":
+ continue
+ if self.bundle_files < 3 and item.__name__ in ("pythoncom", "pywintypes"):
+ # these are handled specially in zipextimporter.
+ continue
+ src = item.__file__
+ if src:
+ base, ext = os.path.splitext(src)
+ suffix = ext
+ if sys.platform.startswith("win") and ext in [".dll", ".pyd"] \
+ and base.endswith("_d"):
+ suffix = "_d" + ext
+
+ if suffix in _py_suffixes:
+ py_files.append(item)
+ elif suffix in _c_suffixes:
+ extensions.append(item)
+ if not self.bundle_files < 3:
+ loader = self.create_loader(item)
+ if loader:
+ py_files.append(loader)
+ else:
+ raise RuntimeError(
+ "Don't know how to handle '%s'" % repr(src))
+ else:
+ builtins.append(item.__name__)
+
+ # sort on the file names, the output is nicer to read
+ py_files.sort(lambda a, b: cmp(a.__file__, b.__file__))
+ extensions.sort(lambda a, b: cmp(a.__file__, b.__file__))
+ builtins.sort()
+ return py_files, extensions, builtins
+
+ def plat_finalize(self, modules, py_files, extensions, dlls):
+ # platform specific code for final adjustments to the file
+ # lists
+ if sys.platform == "win32":
+ # pythoncom and pywintypes are imported via LoadLibrary calls,
+ # help py2exe to include the dlls:
+ if "pythoncom" in list(modules.keys()):
+ import pythoncom
+ dlls.add(pythoncom.__file__)
+ if "pywintypes" in list(modules.keys()):
+ import pywintypes
+ dlls.add(pywintypes.__file__)
+ self.copy_w9xpopen(modules, dlls)
+ else:
+ raise DistutilsError(
+ "Platform %s not yet implemented" % sys.platform)
+
+ def copy_w9xpopen(self, modules, dlls):
+ # Using popen requires (on Win9X) the w9xpopen.exe helper executable.
+ if "os" in list(modules.keys()) or "popen2" in list(modules.keys()):
+ if is_debug_build:
+ fname = os.path.join(os.path.dirname(
+ sys.executable), "w9xpopen_d.exe")
+ else:
+ fname = os.path.join(os.path.dirname(
+ sys.executable), "w9xpopen.exe")
+ # Don't copy w9xpopen.exe if it doesn't exist (64-bit
+ # Python build, for example)
+ if os.path.exists(fname):
+ dlls.add(fname)
+
+ def create_loader(self, item):
+ # Hm, how to avoid needless recreation of this file?
+ pathname = os.path.join(self.temp_dir, "%s.py" % item.__name__)
+ if self.bundle_files > 2: # don't bundle pyds and dlls
+ # all dlls are copied into the same directory, so modify
+ # names to include the package name to avoid name
+ # conflicts and tuck it away for future reference
+ fname = item.__name__ + os.path.splitext(item.__file__)[1]
+ item.__pydfile__ = fname
+ else:
+ fname = os.path.basename(item.__file__)
+
+ # and what about dry_run?
+ if self.verbose:
+ print(("creating python loader for extension '%s' (%s -> %s)" %
+ (item.__name__, item.__file__, fname)))
+
+ source = LOADER % fname
+ if not self.dry_run:
+ open(pathname, "w").write(source)
+ else:
+ return None
+ from modulefinder import Module
+ return Module(item.__name__, pathname)
+
+ def plat_prepare(self):
+ self.includes.append("warnings") # needed by Python itself
+ if not self.ascii:
+ self.packages.append("encodings")
+ self.includes.append("codecs")
+ if self.bundle_files < 3:
+ self.includes.append("zipextimporter")
+ # builtin in run_*.exe and run_*.dll
+ self.excludes.append("_memimporter")
+ if self.compressed:
+ self.includes.append("zlib")
+
+ # os.path will never be found ;-)
+ self.ignores.append('os.path')
+
+ # update the self.ignores list to ignore platform specific
+ # modules.
+ if sys.platform == "win32":
+ self.ignores += ['AL',
+ 'Audio_mac',
+ 'Carbon.File',
+ 'Carbon.Folder',
+ 'Carbon.Folders',
+ 'EasyDialogs',
+ 'MacOS',
+ 'Mailman',
+ 'SOCKS',
+ 'SUNAUDIODEV',
+ '_dummy_threading',
+ '_emx_link',
+ '_xmlplus',
+ '_xmlrpclib',
+ 'al',
+ 'bundlebuilder',
+ 'ce',
+ 'cl',
+ 'dbm',
+ 'dos',
+ 'fcntl',
+ 'gestalt',
+ 'grp',
+ 'ic',
+ 'java.lang',
+ 'mac',
+ 'macfs',
+ 'macostools',
+ 'mkcwproject',
+ 'org.python.core',
+ 'os.path',
+ 'os2',
+ 'poll',
+ 'posix',
+ 'pwd',
+ 'readline',
+ 'riscos',
+ 'riscosenviron',
+ 'riscospath',
+ 'rourl2path',
+ 'sgi',
+ 'sgmlop',
+ 'sunaudiodev',
+ 'termios',
+ 'vms_lib']
+ # special dlls which must be copied to the exe_dir, not the lib_dir
+ self.dlls_in_exedir = [python_dll,
+ "w9xpopen%s.exe" % (
+ is_debug_build and "_d" or ""),
+ "msvcr71%s.dll" % (is_debug_build and "d" or "")]
+ else:
+ raise DistutilsError(
+ "Platform %s not yet implemented" % sys.platform)
+
+ def find_needed_modules(self, mf, files, modules):
+ # feed Modulefinder with everything, and return it.
+ for mod in modules:
+ mf.import_hook(mod)
+
+ for path in files:
+ mf.run_script(path)
+
+ mf.run_script(self.get_boot_script("common"))
+
+ if self.distribution.com_server:
+ mf.run_script(self.get_boot_script("com_servers"))
+
+ if self.distribution.ctypes_com_server:
+ mf.run_script(self.get_boot_script("ctypes_com_server"))
+
+ if self.distribution.service:
+ mf.run_script(self.get_boot_script("service"))
+
+ if self.custom_boot_script:
+ mf.run_script(self.custom_boot_script)
+
+ for mod in self.includes:
+ if mod[-2:] == '.*':
+ mf.import_hook(mod[:-2], None, ['*'])
+ else:
+ mf.import_hook(mod)
+
+ for f in self.packages:
+ def visit(arg, dirname, names):
+ if '__init__.py' in names:
+ arg.append(dirname)
+
+ # Try to find the package using ModuleFinders's method to
+ # allow for modulefinder.AddPackagePath interactions
+ mf.import_hook(f)
+
+ # If modulefinder has seen a reference to the package, then
+ # we prefer to believe that (imp_find_module doesn't seem to locate
+ # sub-packages)
+ if f in mf.modules:
+ module = mf.modules[f]
+ if module.__path__ is None:
+ # it's a module, not a package, so paths contains just the
+ # file entry
+ paths = [module.__file__]
+ else:
+ # it is a package because __path__ is available. __path__
+ # is actually a list of paths that are searched to import
+ # sub-modules and sub-packages
+ paths = module.__path__
+ else:
+ # Find path of package
+ try:
+ paths = [imp_find_module(f)[1]]
+ except ImportError:
+ self.warn("No package named %s" % f)
+ continue
+
+ packages = []
+ for path in paths:
+ # walk the path to find subdirs containing __init__.py files
+ os.path.walk(path, visit, packages)
+
+ # scan the results (directory of __init__.py files)
+ # first trim the path (of the head package),
+ # then convert directory name in package name,
+ # finally push into modulefinder.
+ for p in packages:
+ if p.startswith(path):
+ package = f + '.' + p[len(path)+1:].replace('\\', '.')
+ mf.import_hook(package, None, ["*"])
+
+ return mf
+
+ def make_lib_archive(self, zip_filename, base_dir, files,
+ verbose=0, dry_run=0):
+ from distutils.dir_util import mkpath
+ if not self.skip_archive:
+ # Like distutils "make_archive", but we can specify the files
+ # to include, and the compression to use - default is
+ # ZIP_STORED to keep the runtime performance up. Also, we
+ # don't append '.zip' to the filename.
+ mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
+
+ if self.compressed:
+ compression = zipfile.ZIP_DEFLATED
+ else:
+ compression = zipfile.ZIP_STORED
+
+ if not dry_run:
+ z = zipfile.ZipFile(zip_filename, "w",
+ compression=compression)
+ for f in files:
+ z.write(os.path.join(base_dir, f), f)
+ z.close()
+
+ return zip_filename
+ else:
+ # Don't really produce an archive, just copy the files.
+ from distutils.file_util import copy_file
+
+ destFolder = os.path.dirname(zip_filename)
+
+ for f in files:
+ d = os.path.dirname(f)
+ if d:
+ mkpath(os.path.join(destFolder, d),
+ verbose=verbose, dry_run=dry_run)
+ copy_file(
+ os.path.join(base_dir, f),
+ os.path.join(destFolder, f),
+ preserve_mode=0,
+ verbose=verbose,
+ dry_run=dry_run
+ )
+ return '.'
+
+
+################################################################
+
+class FileSet:
+ # A case insensitive but case preserving set of files
+ def __init__(self, iterable=None):
+ self._dict = {}
+ if iterable is not None:
+ for arg in iterable:
+ self.add(arg)
+
+ def __repr__(self):
+ return "" % (list(self._dict.values()), id(self))
+
+ def add(self, fname):
+ self._dict[fname.upper()] = fname
+
+ def remove(self, fname):
+ del self._dict[fname.upper()]
+
+ def __contains__(self, fname):
+ return fname.upper() in list(self._dict.keys())
+
+ def __getitem__(self, index):
+ key = list(self._dict.keys())[index]
+ return self._dict[key]
+
+ def __len__(self):
+ return len(self._dict)
+
+ def copy(self):
+ res = FileSet()
+ res._dict.update(self._dict)
+ return res
+
+# class FileSet()
+
+
+def bin_depends(path, images, excluded_dlls):
+ import py2exe_util
+ warnings = FileSet()
+ images = FileSet(images)
+ dependents = FileSet()
+ others = FileSet()
+ while images:
+ for image in images.copy():
+ images.remove(image)
+ if not image in dependents:
+ dependents.add(image)
+ abs_image = os.path.abspath(image)
+ loadpath = os.path.dirname(abs_image) + ';' + path
+ for result in list(py2exe_util.depends(image, loadpath).items()):
+ dll, uses_import_module = result
+ if os.path.basename(dll).lower() not in excluded_dlls:
+ if isSystemDLL(dll):
+ others.add(dll)
+ continue
+ if dll not in images and dll not in dependents:
+ images.add(dll)
+ if uses_import_module:
+ warnings.add(dll)
+ return dependents, warnings, others
+
+
+# DLLs to be excluded
+# XXX This list is NOT complete (it cannot be)
+# Note: ALL ENTRIES MUST BE IN LOWER CASE!
+EXCLUDED_DLLS = (
+ "advapi32.dll",
+ "comctl32.dll",
+ "comdlg32.dll",
+ "crtdll.dll",
+ "gdi32.dll",
+ "glu32.dll",
+ "opengl32.dll",
+ "imm32.dll",
+ "kernel32.dll",
+ "mfc42.dll",
+ "msvcirt.dll",
+ "msvcrt.dll",
+ "msvcrtd.dll",
+ "ntdll.dll",
+ "odbc32.dll",
+ "ole32.dll",
+ "oleaut32.dll",
+ "rpcrt4.dll",
+ "shell32.dll",
+ "shlwapi.dll",
+ "user32.dll",
+ "version.dll",
+ "winmm.dll",
+ "winspool.drv",
+ "ws2_32.dll",
+ "ws2help.dll",
+ "wsock32.dll",
+ "netapi32.dll",
+
+ "gdiplus.dll",
+)
+
+# XXX Perhaps it would be better to assume dlls from the systemdir are system dlls,
+# and make some exceptions for known dlls, like msvcr71, pythonXY.dll, and so on?
+
+
+def isSystemDLL(pathname):
+ if os.path.basename(pathname).lower() in ("msvcr71.dll", "msvcr71d.dll"):
+ return 0
+ if os.path.basename(pathname).lower() in EXCLUDED_DLLS:
+ return 1
+ # How can we determine whether a dll is a 'SYSTEM DLL'?
+ # Is it sufficient to use the Image Load Address?
+ import struct
+ file = open(pathname, "rb")
+ if file.read(2) != "MZ":
+ raise Exception("Seems not to be an exe-file")
+ file.seek(0x3C)
+ pe_ofs = struct.unpack("i", file.read(4))[0]
+ file.seek(pe_ofs)
+ if file.read(4) != "PE\000\000":
+ raise Exception("Seems not to be an exe-file", pathname)
+ # COFF File Header, offset of ImageBase in Optional Header
+ file.read(20 + 28)
+ imagebase = struct.unpack("I", file.read(4))[0]
+ return not (imagebase < 0x70000000)
+
+
+def byte_compile(py_files, optimize=0, force=0,
+ target_dir=None, verbose=1, dry_run=0,
+ direct=None):
+
+ if direct is None:
+ direct = (__debug__ and optimize == 0)
+
+ # "Indirect" byte-compilation: write a temporary script and then
+ # run it with the appropriate flags.
+ if not direct:
+ from tempfile import mktemp
+ from distutils.util import execute
+ script_name = mktemp(".py")
+ if verbose:
+ print(("writing byte-compilation script '%s'" % script_name))
+ if not dry_run:
+ script = open(script_name, "w")
+ script.write("""\
+from py2exe.build_exe import byte_compile
+from modulefinder import Module
+files = [
+""")
+
+ for f in py_files:
+ script.write("Module(%s, %s, %s),\n" %
+ (repr(f.__name__), repr(f.__file__), repr(f.__path__)))
+ script.write("]\n")
+ script.write("""
+byte_compile(files, optimize=%s, force=%s,
+ target_dir=%s,
+ verbose=%s, dry_run=0,
+ direct=1)
+""" % (repr(optimize), repr(force), repr(target_dir), repr(verbose)))
+
+ script.close()
+
+ cmd = [sys.executable, script_name]
+ if optimize == 1:
+ cmd.insert(1, "-O")
+ elif optimize == 2:
+ cmd.insert(1, "-OO")
+ spawn(cmd, verbose=verbose, dry_run=dry_run)
+ execute(os.remove, (script_name,), "removing %s" % script_name,
+ verbose=verbose, dry_run=dry_run)
+
+ else:
+ from py_compile import compile
+ from distutils.dir_util import mkpath
+ from distutils.dep_util import newer
+ from distutils.file_util import copy_file
+
+ for file in py_files:
+ # Terminology from the py_compile module:
+ # cfile - byte-compiled file
+ # dfile - purported source filename (same as 'file' by default)
+ cfile = file.__name__.replace('.', '\\')
+
+ if file.__path__:
+ dfile = cfile + '\\__init__.py' + (__debug__ and 'c' or 'o')
+ else:
+ dfile = cfile + '.py' + (__debug__ and 'c' or 'o')
+ if target_dir:
+ cfile = os.path.join(target_dir, dfile)
+
+ if force or newer(file.__file__, cfile):
+ if verbose:
+ print(("byte-compiling %s to %s" % (file.__file__, dfile)))
+ if not dry_run:
+ mkpath(os.path.dirname(cfile))
+ suffix = os.path.splitext(file.__file__)[1]
+ if suffix in (".py", ".pyw"):
+ compile(file.__file__, cfile, dfile)
+ elif suffix in _py_suffixes:
+ # Minor problem: This will happily copy a file
+ # .pyo to .pyc or .pyc to
+ # .pyo, but it does seem to work.
+ copy_file(file.__file__, cfile, preserve_mode=0)
+ else:
+ raise RuntimeError(
+ "Don't know how to handle %r" % file.__file__)
+ else:
+ if verbose:
+ print(("skipping byte-compilation of %s to %s" %
+ (file.__file__, dfile)))
+ compiled_files = []
+ for file in py_files:
+ cfile = file.__name__.replace('.', '\\')
+
+ if file.__path__:
+ dfile = cfile + '\\__init__.py' + (optimize and 'o' or 'c')
+ else:
+ dfile = cfile + '.py' + (optimize and 'o' or 'c')
+ compiled_files.append(dfile)
+ return compiled_files
+
+# byte_compile()
+
+# win32com makepy helper.
+
+
+def collect_win32com_genpy(path, typelibs, verbose=0, dry_run=0):
+ import win32com
+ from win32com.client import gencache, makepy
+ from distutils.file_util import copy_file
+
+ old_gen_path = win32com.__gen_path__
+ num = 0
+ try:
+ win32com.__gen_path__ = path
+ win32com.gen_py.__path__ = [path]
+ gencache.__init__()
+ for info in typelibs:
+ guid, lcid, major, minor = info[:4]
+ # They may provide an input filename in the tuple - in which case
+ # they will have pre-generated it on a machine with the typelibs
+ # installed, and just want us to include it.
+ fname_in = None
+ if len(info) > 4:
+ fname_in = info[4]
+ if fname_in is not None:
+ base = gencache.GetGeneratedFileName(guid, lcid, major, minor)
+ fname_out = os.path.join(path, base) + ".py"
+ copy_file(fname_in, fname_out,
+ verbose=verbose, dry_run=dry_run)
+ num += 1
+ # That's all we gotta do!
+ continue
+
+ # It seems bForDemand=True generates code which is missing
+ # at least sometimes an import of DispatchBaseClass.
+ # Until this is resolved, set it to false.
+ # What's the purpose of bForDemand=True? Thomas
+ # bForDemand is supposed to only generate stubs when each
+ # individual object is referenced. A side-effect of that is
+ # that each object gets its own source file. The intent of
+ # this code was to set bForDemand=True, meaning we get the
+ # 'file per object' behaviour, but then explicitly walk all
+ # children forcing them to be built - so the entire object model
+ # is included, but not in a huge .pyc.
+ # I'm not sure why its not working :) I'll debug later.
+ # bForDemand=False isn't really important here - the overhead for
+ # monolithic typelib stubs is in the compilation, not the loading
+ # of an existing .pyc. Mark.
+## makepy.GenerateFromTypeLibSpec(info, bForDemand = True)
+ tlb_info = (guid, lcid, major, minor)
+ makepy.GenerateFromTypeLibSpec(tlb_info, bForDemand=False)
+ # Now get the module, and build all sub-modules.
+ mod = gencache.GetModuleForTypelib(*tlb_info)
+ for clsid, name in list(mod.CLSIDToPackageMap.items()):
+ try:
+ gencache.GetModuleForCLSID(clsid)
+ num += 1
+ # print "", name
+ except ImportError:
+ pass
+ return num
+ finally:
+ # restore win32com, just in case.
+ win32com.__gen_path__ = old_gen_path
+ win32com.gen_py.__path__ = [old_gen_path]
+ gencache.__init__()
+
+# utilities hacked from distutils.dir_util
+
+
+def _chmod(file):
+ os.chmod(file, 0o777)
+
+# Helper for force_remove_tree()
+
+
+def _build_cmdtuple(path, cmdtuples):
+ for f in os.listdir(path):
+ real_f = os.path.join(path, f)
+ if os.path.isdir(real_f) and not os.path.islink(real_f):
+ _build_cmdtuple(real_f, cmdtuples)
+ else:
+ cmdtuples.append((_chmod, real_f))
+ cmdtuples.append((os.remove, real_f))
+ cmdtuples.append((os.rmdir, path))
+
+
+def force_remove_tree(directory, verbose=0, dry_run=0):
+ """Recursively remove an entire directory tree. Any errors are ignored
+ (apart from being reported to stdout if 'verbose' is true).
+ """
+ import distutils
+ from distutils.util import grok_environment_error
+ _path_created = distutils.dir_util._path_created
+
+ if verbose:
+ print(("removing '%s' (and everything under it)" % directory))
+ if dry_run:
+ return
+ cmdtuples = []
+ _build_cmdtuple(directory, cmdtuples)
+ for cmd in cmdtuples:
+ try:
+ cmd[0](cmd[1])
+ # remove dir from cache if it's already there
+ abspath = os.path.abspath(cmd[1])
+ if abspath in _path_created:
+ del _path_created[abspath]
+ except (IOError, OSError) as exc:
+ if verbose:
+ print((grok_environment_error(
+ exc, "error removing %s: " % directory)))
diff --git a/patch/mf.py b/patch/mf.py
index acfa33e..2ce540d 100644
--- a/patch/mf.py
+++ b/patch/mf.py
@@ -1,812 +1,832 @@
-"""Find modules used by a script, using introspection."""
-# This module should be kept compatible with Python 2.2, see PEP 291.
-
-from __future__ import generators
-import dis
-import imp
-import marshal
-import os
-import sys
-import types
-import struct
-
-if hasattr(sys.__stdout__, "newlines"):
- READ_MODE = "U" # universal line endings
-else:
- # remain compatible with Python < 2.3
- READ_MODE = "r"
-
-LOAD_CONST = chr(dis.opname.index('LOAD_CONST'))
-IMPORT_NAME = chr(dis.opname.index('IMPORT_NAME'))
-STORE_NAME = chr(dis.opname.index('STORE_NAME'))
-STORE_GLOBAL = chr(dis.opname.index('STORE_GLOBAL'))
-STORE_OPS = [STORE_NAME, STORE_GLOBAL]
-HAVE_ARGUMENT = chr(dis.HAVE_ARGUMENT)
-
-# !!! NOTE BEFORE INCLUDING IN PYTHON DISTRIBUTION !!!
-# To clear up issues caused by the duplication of data structures between
-# the real Python modulefinder and this duplicate version, packagePathMap
-# and replacePackageMap are imported from the actual modulefinder. This
-# should be changed back to the assigments that are commented out below.
-# There are also py2exe specific pieces at the bottom of this file.
-
-
-# Modulefinder does a good job at simulating Python's, but it can not
-# handle __path__ modifications packages make at runtime. Therefore there
-# is a mechanism whereby you can register extra paths in this map for a
-# package, and it will be honored.
-
-# Note this is a mapping is lists of paths.
-#~ packagePathMap = {}
-from modulefinder import packagePathMap
-
-# A Public interface
-def AddPackagePath(packagename, path):
- paths = packagePathMap.get(packagename, [])
- paths.append(path)
- packagePathMap[packagename] = paths
-
-#~ replacePackageMap = {}
-from modulefinder import replacePackageMap
-
-# This ReplacePackage mechanism allows modulefinder to work around the
-# way the _xmlplus package injects itself under the name "xml" into
-# sys.modules at runtime by calling ReplacePackage("_xmlplus", "xml")
-# before running ModuleFinder.
-
-def ReplacePackage(oldname, newname):
- replacePackageMap[oldname] = newname
-
-
-class Module:
-
- def __init__(self, name, file=None, path=None):
- self.__name__ = name
- self.__file__ = file
- self.__path__ = path
- self.__code__ = None
- # The set of global names that are assigned to in the module.
- # This includes those names imported through starimports of
- # Python modules.
- self.globalnames = {}
- # The set of starimports this module did that could not be
- # resolved, ie. a starimport from a non-Python module.
- self.starimports = {}
-
- def __repr__(self):
- s = "Module(%r" % (self.__name__,)
- if self.__file__ is not None:
- s = s + ", %r" % (self.__file__,)
- if self.__path__ is not None:
- s = s + ", %r" % (self.__path__,)
- s = s + ")"
- return s
-
-class ModuleFinder:
-
- def __init__(self, path=None, debug=0, excludes=[], replace_paths=[], skip_scan=[]):
- if path is None:
- path = sys.path
- self.path = path
- self.modules = {}
- self.badmodules = {}
- self.debug = debug
- self.indent = 0
- self.excludes = excludes
- self.replace_paths = replace_paths
- self.skip_scan = skip_scan
- self.processed_paths = [] # Used in debugging only
-
- def msg(self, level, str, *args):
- if level <= self.debug:
- for i in range(self.indent):
- print " ",
- print str,
- for arg in args:
- print repr(arg),
- print
-
- def msgin(self, *args):
- level = args[0]
- if level <= self.debug:
- self.indent = self.indent + 1
- self.msg(*args)
-
- def msgout(self, *args):
- level = args[0]
- if level <= self.debug:
- self.indent = self.indent - 1
- self.msg(*args)
-
- def run_script(self, pathname):
- self.msg(2, "run_script", pathname)
- fp = open(pathname, READ_MODE)
- stuff = ("", "r", imp.PY_SOURCE)
- self.load_module('__main__', fp, pathname, stuff)
-
- def load_file(self, pathname):
- dir, name = os.path.split(pathname)
- name, ext = os.path.splitext(name)
- fp = open(pathname, READ_MODE)
- stuff = (ext, "r", imp.PY_SOURCE)
- self.load_module(name, fp, pathname, stuff)
-
- def import_hook(self, name, caller=None, fromlist=None, level=-1):
- self.msg(3, "import_hook", name, caller, fromlist, level)
- parent = self.determine_parent(caller, level=level)
- q, tail = self.find_head_package(parent, name)
- m = self.load_tail(q, tail)
- if not fromlist:
- return q
- if m.__path__:
- self.ensure_fromlist(m, fromlist)
- return None
-
- def determine_parent(self, caller, level=-1):
- self.msgin(4, "determine_parent", caller, level)
- if not caller or level == 0:
- self.msgout(4, "determine_parent -> None")
- return None
- pname = caller.__name__
- if level >= 1: # relative import
- if caller.__path__:
- level -= 1
- if level == 0:
- parent = self.modules[pname]
- assert parent is caller
- self.msgout(4, "determine_parent ->", parent)
- return parent
- if pname.count(".") < level:
- raise ImportError, "relative importpath too deep"
- pname = ".".join(pname.split(".")[:-level])
- parent = self.modules[pname]
- self.msgout(4, "determine_parent ->", parent)
- return parent
- if caller.__path__:
- parent = self.modules[pname]
- assert caller is parent
- self.msgout(4, "determine_parent ->", parent)
- return parent
- if '.' in pname:
- i = pname.rfind('.')
- pname = pname[:i]
- parent = self.modules[pname]
- assert parent.__name__ == pname
- self.msgout(4, "determine_parent ->", parent)
- return parent
- self.msgout(4, "determine_parent -> None")
- return None
-
- def find_head_package(self, parent, name):
- self.msgin(4, "find_head_package", parent, name)
- if '.' in name:
- i = name.find('.')
- head = name[:i]
- tail = name[i+1:]
- else:
- head = name
- tail = ""
- if parent:
- qname = "%s.%s" % (parent.__name__, head)
- else:
- qname = head
- q = self.import_module(head, qname, parent)
- if q:
- self.msgout(4, "find_head_package ->", (q, tail))
- return q, tail
- if parent:
- qname = head
- parent = None
- q = self.import_module(head, qname, parent)
- if q:
- self.msgout(4, "find_head_package ->", (q, tail))
- return q, tail
- self.msgout(4, "raise ImportError: No module named", qname)
- raise ImportError, "No module named " + qname
-
- def load_tail(self, q, tail):
- self.msgin(4, "load_tail", q, tail)
- m = q
- while tail:
- i = tail.find('.')
- if i < 0: i = len(tail)
- head, tail = tail[:i], tail[i+1:]
- mname = "%s.%s" % (m.__name__, head)
- m = self.import_module(head, mname, m)
- if not m:
- self.msgout(4, "raise ImportError: No module named", mname)
- raise ImportError, "No module named " + mname
- self.msgout(4, "load_tail ->", m)
- return m
-
- def ensure_fromlist(self, m, fromlist, recursive=0):
- self.msg(4, "ensure_fromlist", m, fromlist, recursive)
- for sub in fromlist:
- if sub == "*":
- if not recursive:
- all = self.find_all_submodules(m)
- if all:
- self.ensure_fromlist(m, all, 1)
- elif not hasattr(m, sub):
- subname = "%s.%s" % (m.__name__, sub)
- submod = self.import_module(sub, subname, m)
- if not submod:
- raise ImportError, "No module named " + subname
-
- def find_all_submodules(self, m):
- if not m.__path__:
- return
- modules = {}
- # 'suffixes' used to be a list hardcoded to [".py", ".pyc", ".pyo"].
- # But we must also collect Python extension modules - although
- # we cannot separate normal dlls from Python extensions.
- suffixes = []
- for triple in imp.get_suffixes():
- suffixes.append(triple[0])
- for dir in m.__path__:
- try:
- names = os.listdir(dir)
- except os.error:
- self.msg(2, "can't list directory", dir)
- continue
- for name in names:
- mod = None
- for suff in suffixes:
- n = len(suff)
- if name[-n:] == suff:
- mod = name[:-n]
- break
- if mod and mod != "__init__":
- modules[mod] = mod
- return modules.keys()
-
- def import_module(self, partname, fqname, parent):
- self.msgin(3, "import_module", partname, fqname, parent)
- try:
- m = self.modules[fqname]
- except KeyError:
- pass
- else:
- self.msgout(3, "import_module ->", m)
- return m
- if self.badmodules.has_key(fqname):
- self.msgout(3, "import_module -> None")
- return None
- if parent and parent.__path__ is None:
- self.msgout(3, "import_module -> None")
- return None
- try:
- fp, pathname, stuff = self.find_module(partname,
- parent and parent.__path__, parent)
- except ImportError:
- self.msgout(3, "import_module ->", None)
- return None
- try:
- m = self.load_module(fqname, fp, pathname, stuff)
- finally:
- if fp: fp.close()
- if parent:
- setattr(parent, partname, m)
- self.msgout(3, "import_module ->", m)
- return m
-
- def load_module(self, fqname, fp, pathname, (suffix, mode, type)):
- self.msgin(2, "load_module", fqname, fp and "fp", pathname)
- if type == imp.PKG_DIRECTORY:
- m = self.load_package(fqname, pathname)
- self.msgout(2, "load_module ->", m)
- return m
- if type == imp.PY_SOURCE:
- co = compile(fp.read()+'\n', pathname, 'exec')
- elif type == imp.PY_COMPILED:
- if fp.read(4) != imp.get_magic():
- self.msgout(2, "raise ImportError: Bad magic number", pathname)
- raise ImportError, "Bad magic number in %s" % pathname
- fp.read(4)
- co = marshal.load(fp)
- else:
- co = None
- m = self.add_module(fqname)
- m.__file__ = pathname
- if co:
- if self.replace_paths:
- co = self.replace_paths_in_code(co)
- m.__code__ = co
- self.scan_code(co, m)
- self.msgout(2, "load_module ->", m)
- return m
-
- def _add_badmodule(self, name, caller):
- if name not in self.badmodules:
- self.badmodules[name] = {}
- if caller:
- self.badmodules[name][caller.__name__] = 1
- else:
- self.badmodules[name]["-"] = 1
-
- def _safe_import_hook(self, name, caller, fromlist, level=-1):
- # wrapper for self.import_hook() that won't raise ImportError
- if name in self.badmodules:
- self._add_badmodule(name, caller)
- return
- try:
- self.import_hook(name, caller, level=level)
- except ImportError, msg:
- self.msg(2, "ImportError:", str(msg))
- self._add_badmodule(name, caller)
- else:
- if fromlist:
- for sub in fromlist:
- if sub in self.badmodules:
- self._add_badmodule(sub, caller)
- continue
- try:
- self.import_hook(name, caller, [sub], level=level)
- except ImportError, msg:
- self.msg(2, "ImportError:", str(msg))
- fullname = name + "." + sub
- self._add_badmodule(fullname, caller)
-
- def scan_opcodes(self, co,
- unpack = struct.unpack):
- # Scan the code, and yield 'interesting' opcode combinations
- # Version for Python 2.4 and older
- code = co.co_code
- names = co.co_names
- consts = co.co_consts
- while code:
- c = code[0]
- if c in STORE_OPS:
- oparg, = unpack('= HAVE_ARGUMENT:
- code = code[3:]
- else:
- code = code[1:]
-
- def scan_opcodes_25(self, co,
- unpack = struct.unpack):
- # Scan the code, and yield 'interesting' opcode combinations
- # Python 2.5 version (has absolute and relative imports)
- code = co.co_code
- names = co.co_names
- consts = co.co_consts
- LOAD_LOAD_AND_IMPORT = LOAD_CONST + LOAD_CONST + IMPORT_NAME
- while code:
- c = code[0]
- if c in STORE_OPS:
- oparg, = unpack('= HAVE_ARGUMENT:
- code = code[3:]
- else:
- code = code[1:]
-
- def scan_code(self, co, m):
- if m.__name__ in self.skip_scan:
- return
- code = co.co_code
- if sys.version_info >= (2, 5):
- scanner = self.scan_opcodes_25
- else:
- scanner = self.scan_opcodes
- for what, args in scanner(co):
- if what == "store":
- name, = args
- m.globalnames[name] = 1
- elif what in ("import", "absolute_import"):
- fromlist, name = args
- have_star = 0
- if fromlist is not None:
- if "*" in fromlist:
- have_star = 1
- fromlist = [f for f in fromlist if f != "*"]
- if what == "absolute_import": level = 0
- else: level = -1
- self._safe_import_hook(name, m, fromlist, level=level)
- if have_star:
- # We've encountered an "import *". If it is a Python module,
- # the code has already been parsed and we can suck out the
- # global names.
- mm = None
- if m.__path__:
- # At this point we don't know whether 'name' is a
- # submodule of 'm' or a global module. Let's just try
- # the full name first.
- mm = self.modules.get(m.__name__ + "." + name)
- if mm is None:
- mm = self.modules.get(name)
- if mm is not None:
- m.globalnames.update(mm.globalnames)
- m.starimports.update(mm.starimports)
- if mm.__code__ is None:
- m.starimports[name] = 1
- else:
- m.starimports[name] = 1
- elif what == "relative_import":
- level, fromlist, name = args
- if name:
- self._safe_import_hook(name, m, fromlist, level=level)
- else:
- parent = self.determine_parent(m, level=level)
- self._safe_import_hook(parent.__name__, None, fromlist, level=0)
- else:
- # We don't expect anything else from the generator.
- raise RuntimeError(what)
-
- for c in co.co_consts:
- if isinstance(c, type(co)):
- self.scan_code(c, m)
-
- def load_package(self, fqname, pathname):
- self.msgin(2, "load_package", fqname, pathname)
- newname = replacePackageMap.get(fqname)
- if newname:
- fqname = newname
- m = self.add_module(fqname)
- m.__file__ = pathname
- m.__path__ = [pathname]
-
- # As per comment at top of file, simulate runtime __path__ additions.
- m.__path__ = m.__path__ + packagePathMap.get(fqname, [])
-
- fp, buf, stuff = self.find_module("__init__", m.__path__)
- self.load_module(fqname, fp, buf, stuff)
- self.msgout(2, "load_package ->", m)
- return m
-
- def add_module(self, fqname):
- if self.modules.has_key(fqname):
- return self.modules[fqname]
- self.modules[fqname] = m = Module(fqname)
- return m
-
- def find_module(self, name, path, parent=None):
- if parent is not None:
- # assert path is not None
- fullname = parent.__name__+'.'+name
- else:
- fullname = name
- if fullname in self.excludes:
- self.msgout(3, "find_module -> Excluded", fullname)
- raise ImportError, name
-
- if path is None:
- if name in sys.builtin_module_names:
- return (None, None, ("", "", imp.C_BUILTIN))
-
- path = self.path
- return imp.find_module(name, path)
-
- def report(self):
- """Print a report to stdout, listing the found modules with their
- paths, as well as modules that are missing, or seem to be missing.
- """
- print
- print " %-25s %s" % ("Name", "File")
- print " %-25s %s" % ("----", "----")
- # Print modules found
- keys = self.modules.keys()
- keys.sort()
- for key in keys:
- m = self.modules[key]
- if m.__path__:
- print "P",
- else:
- print "m",
- print "%-25s" % key, m.__file__ or ""
-
- # Print missing modules
- missing, maybe = self.any_missing_maybe()
- if missing:
- print
- print "Missing modules:"
- for name in missing:
- mods = self.badmodules[name].keys()
- mods.sort()
- print "?", name, "imported from", ', '.join(mods)
- # Print modules that may be missing, but then again, maybe not...
- if maybe:
- print
- print "Submodules thay appear to be missing, but could also be",
- print "global names in the parent package:"
- for name in maybe:
- mods = self.badmodules[name].keys()
- mods.sort()
- print "?", name, "imported from", ', '.join(mods)
-
- def any_missing(self):
- """Return a list of modules that appear to be missing. Use
- any_missing_maybe() if you want to know which modules are
- certain to be missing, and which *may* be missing.
- """
- missing, maybe = self.any_missing_maybe()
- return missing + maybe
-
- def any_missing_maybe(self):
- """Return two lists, one with modules that are certainly missing
- and one with modules that *may* be missing. The latter names could
- either be submodules *or* just global names in the package.
-
- The reason it can't always be determined is that it's impossible to
- tell which names are imported when "from module import *" is done
- with an extension module, short of actually importing it.
- """
- missing = []
- maybe = []
- for name in self.badmodules:
- if name in self.excludes:
- continue
- i = name.rfind(".")
- if i < 0:
- missing.append(name)
- continue
- subname = name[i+1:]
- pkgname = name[:i]
- pkg = self.modules.get(pkgname)
- if pkg is not None:
- if pkgname in self.badmodules[name]:
- # The package tried to import this module itself and
- # failed. It's definitely missing.
- missing.append(name)
- elif subname in pkg.globalnames:
- # It's a global in the package: definitely not missing.
- pass
- elif pkg.starimports:
- # It could be missing, but the package did an "import *"
- # from a non-Python module, so we simply can't be sure.
- maybe.append(name)
- else:
- # It's not a global in the package, the package didn't
- # do funny star imports, it's very likely to be missing.
- # The symbol could be inserted into the package from the
- # outside, but since that's not good style we simply list
- # it missing.
- missing.append(name)
- else:
- missing.append(name)
- missing.sort()
- maybe.sort()
- return missing, maybe
-
- def replace_paths_in_code(self, co):
- new_filename = original_filename = os.path.normpath(co.co_filename)
- for f, r in self.replace_paths:
- if original_filename.startswith(f):
- new_filename = r + original_filename[len(f):]
- break
-
- if self.debug and original_filename not in self.processed_paths:
- if new_filename != original_filename:
- self.msgout(2, "co_filename %r changed to %r" \
- % (original_filename,new_filename,))
- else:
- self.msgout(2, "co_filename %r remains unchanged" \
- % (original_filename,))
- self.processed_paths.append(original_filename)
-
- consts = list(co.co_consts)
- for i in range(len(consts)):
- if isinstance(consts[i], type(co)):
- consts[i] = self.replace_paths_in_code(consts[i])
-
- return types.CodeType(co.co_argcount, co.co_nlocals, co.co_stacksize,
- co.co_flags, co.co_code, tuple(consts), co.co_names,
- co.co_varnames, new_filename, co.co_name,
- co.co_firstlineno, co.co_lnotab,
- co.co_freevars, co.co_cellvars)
-
-
-def test():
- # Parse command line
- import getopt
- try:
- opts, args = getopt.getopt(sys.argv[1:], "dmp:qx:")
- except getopt.error, msg:
- print msg
- return
-
- # Process options
- debug = 1
- domods = 0
- addpath = []
- exclude = []
- for o, a in opts:
- if o == '-d':
- debug = debug + 1
- if o == '-m':
- domods = 1
- if o == '-p':
- addpath = addpath + a.split(os.pathsep)
- if o == '-q':
- debug = 0
- if o == '-x':
- exclude.append(a)
-
- # Provide default arguments
- if not args:
- script = "hello.py"
- else:
- script = args[0]
-
- # Set the path based on sys.path and the script directory
- path = sys.path[:]
- path[0] = os.path.dirname(script)
- path = addpath + path
- if debug > 1:
- print "path:"
- for item in path:
- print " ", repr(item)
-
- # Create the module finder and turn its crank
- mf = ModuleFinder(path, debug, exclude)
- for arg in args[1:]:
- if arg == '-m':
- domods = 1
- continue
- if domods:
- if arg[-2:] == '.*':
- mf.import_hook(arg[:-2], None, ["*"])
- else:
- mf.import_hook(arg)
- else:
- mf.load_file(arg)
- mf.run_script(script)
- mf.report()
- return mf # for -i debugging
-
-
-if __name__ == '__main__':
- try:
- mf = test()
- except KeyboardInterrupt:
- print "\n[interrupt]"
-
-
-
-# py2exe specific portion - this should be removed before inclusion in the
-# Python distribution
-
-import tempfile
-import urllib
-
-try:
- set
-except NameError:
- from sets import Set as set
-
-Base = ModuleFinder
-del ModuleFinder
-
-# Much inspired by Toby Dickenson's code:
-# http://www.tarind.com/depgraph.html
-class ModuleFinder(Base):
- def __init__(self, *args, **kw):
- self._depgraph = {}
- self._types = {}
- self._last_caller = None
- self._scripts = set()
- Base.__init__(self, *args, **kw)
-
- def run_script(self, pathname):
- # Scripts always end in the __main__ module, but we possibly
- # have more than one script in py2exe, so we want to keep
- # *all* the pathnames.
- self._scripts.add(pathname)
- Base.run_script(self, pathname)
-
- def import_hook(self, name, caller=None, fromlist=None, level=-1):
- old_last_caller = self._last_caller
- try:
- self._last_caller = caller
- return Base.import_hook(self,name,caller,fromlist,level)
- finally:
- self._last_caller = old_last_caller
-
- def import_module(self,partnam,fqname,parent):
- r = Base.import_module(self,partnam,fqname,parent)
- if r is not None and self._last_caller:
- self._depgraph.setdefault(self._last_caller.__name__, set()).add(r.__name__)
- return r
-
- def load_module(self, fqname, fp, pathname, (suffix, mode, typ)):
- r = Base.load_module(self, fqname, fp, pathname, (suffix, mode, typ))
- if r is not None:
- self._types[r.__name__] = typ
- return r
-
- def create_xref(self):
- # this code probably needs cleanup
- depgraph = {}
- importedby = {}
- for name, value in self._depgraph.items():
- depgraph[name] = list(value)
- for needs in value:
- importedby.setdefault(needs, set()).add(name)
-
- names = self._types.keys()
- names.sort()
-
- fd, htmlfile = tempfile.mkstemp(".html")
- ofi = open(htmlfile, "w")
- os.close(fd)
- print >> ofi, "py2exe cross reference for %s" % sys.argv[0]
-
- print >> ofi, "
py2exe cross reference for %s
" % sys.argv[0]
-
- for name in names:
- if self._types[name] in (imp.PY_SOURCE, imp.PKG_DIRECTORY):
- print >> ofi, '%s' % (name, name)
- if name == "__main__":
- for fname in self._scripts:
- path = urllib.pathname2url(os.path.abspath(fname))
- print >> ofi, '%s ' \
- % (path, fname)
- print >> ofi, ' imports:'
- else:
- fname = urllib.pathname2url(self.modules[name].__file__)
- print >> ofi, '%s imports:' \
- % (fname, self.modules[name].__file__)
- else:
- fname = self.modules[name].__file__
- if fname:
- print >> ofi, '%s%s imports:' \
- % (name, name, fname)
- else:
- print >> ofi, '%s%s imports:' \
- % (name, name, TYPES[self._types[name]])
-
- if name in depgraph:
- needs = depgraph[name]
- for n in needs:
- print >> ofi, '%s ' % (n, n)
- print >> ofi, " \n"
-
- print >> ofi, 'imported by:'
- if name in importedby:
- for i in importedby[name]:
- print >> ofi, '%s ' % (i, i)
-
- print >> ofi, " \n"
-
- print >> ofi, " \n"
-
- print >> ofi, ""
- ofi.close()
- os.startfile(htmlfile)
- # how long does it take to start the browser?
- import threading
- threading.Timer(5, os.remove, args=[htmlfile])
-
-
-TYPES = {imp.C_BUILTIN: "(builtin module)",
- imp.C_EXTENSION: "extension module",
- imp.IMP_HOOK: "IMP_HOOK",
- imp.PKG_DIRECTORY: "package directory",
- imp.PY_CODERESOURCE: "PY_CODERESOURCE",
- imp.PY_COMPILED: "compiled python module",
- imp.PY_FROZEN: "frozen module",
- imp.PY_RESOURCE: "PY_RESOURCE",
- imp.PY_SOURCE: "python module",
- imp.SEARCH_ERROR: "SEARCH_ERROR"
- }
+"""Find modules used by a script, using introspection."""
+# This module should be kept compatible with Python 2.2, see PEP 291.
+
+
+import urllib.error
+import urllib.parse
+import urllib.request
+import tempfile
+from modulefinder import replacePackageMap
+from modulefinder import packagePathMap
+import dis
+import imp
+import marshal
+import os
+import sys
+import types
+import struct
+
+if hasattr(sys.__stdout__, "newlines"):
+ READ_MODE = "U" # universal line endings
+else:
+ # remain compatible with Python < 2.3
+ READ_MODE = "r"
+
+LOAD_CONST = chr(dis.opname.index('LOAD_CONST'))
+IMPORT_NAME = chr(dis.opname.index('IMPORT_NAME'))
+STORE_NAME = chr(dis.opname.index('STORE_NAME'))
+STORE_GLOBAL = chr(dis.opname.index('STORE_GLOBAL'))
+STORE_OPS = [STORE_NAME, STORE_GLOBAL]
+HAVE_ARGUMENT = chr(dis.HAVE_ARGUMENT)
+
+# !!! NOTE BEFORE INCLUDING IN PYTHON DISTRIBUTION !!!
+# To clear up issues caused by the duplication of data structures between
+# the real Python modulefinder and this duplicate version, packagePathMap
+# and replacePackageMap are imported from the actual modulefinder. This
+# should be changed back to the assigments that are commented out below.
+# There are also py2exe specific pieces at the bottom of this file.
+
+
+# Modulefinder does a good job at simulating Python's, but it can not
+# handle __path__ modifications packages make at runtime. Therefore there
+# is a mechanism whereby you can register extra paths in this map for a
+# package, and it will be honored.
+
+# Note this is a mapping is lists of paths.
+# ~ packagePathMap = {}
+
+# A Public interface
+
+def AddPackagePath(packagename, path):
+ paths = packagePathMap.get(packagename, [])
+ paths.append(path)
+ packagePathMap[packagename] = paths
+
+
+# ~ replacePackageMap = {}
+
+# This ReplacePackage mechanism allows modulefinder to work around the
+# way the _xmlplus package injects itself under the name "xml" into
+# sys.modules at runtime by calling ReplacePackage("_xmlplus", "xml")
+# before running ModuleFinder.
+
+
+def ReplacePackage(oldname, newname):
+ replacePackageMap[oldname] = newname
+
+
+class Module:
+
+ def __init__(self, name, file=None, path=None):
+ self.__name__ = name
+ self.__file__ = file
+ self.__path__ = path
+ self.__code__ = None
+ # The set of global names that are assigned to in the module.
+ # This includes those names imported through starimports of
+ # Python modules.
+ self.globalnames = {}
+ # The set of starimports this module did that could not be
+ # resolved, ie. a starimport from a non-Python module.
+ self.starimports = {}
+
+ def __repr__(self):
+ s = "Module(%r" % (self.__name__,)
+ if self.__file__ is not None:
+ s = s + ", %r" % (self.__file__,)
+ if self.__path__ is not None:
+ s = s + ", %r" % (self.__path__,)
+ s = s + ")"
+ return s
+
+
+class ModuleFinder:
+
+ def __init__(self, path=None, debug=0, excludes=[], replace_paths=[], skip_scan=[]):
+ if path is None:
+ path = sys.path
+ self.path = path
+ self.modules = {}
+ self.badmodules = {}
+ self.debug = debug
+ self.indent = 0
+ self.excludes = excludes
+ self.replace_paths = replace_paths
+ self.skip_scan = skip_scan
+ self.processed_paths = [] # Used in debugging only
+
+ def msg(self, level, str, *args):
+ if level <= self.debug:
+ for i in range(self.indent):
+ print(" ", end=' ')
+ print(str, end=' ')
+ for arg in args:
+ print(repr(arg), end=' ')
+ print()
+
+ def msgin(self, *args):
+ level = args[0]
+ if level <= self.debug:
+ self.indent = self.indent + 1
+ self.msg(*args)
+
+ def msgout(self, *args):
+ level = args[0]
+ if level <= self.debug:
+ self.indent = self.indent - 1
+ self.msg(*args)
+
+ def run_script(self, pathname):
+ self.msg(2, "run_script", pathname)
+ fp = open(pathname, READ_MODE)
+ stuff = ("", "r", imp.PY_SOURCE)
+ self.load_module('__main__', fp, pathname, stuff)
+
+ def load_file(self, pathname):
+ dir, name = os.path.split(pathname)
+ name, ext = os.path.splitext(name)
+ fp = open(pathname, READ_MODE)
+ stuff = (ext, "r", imp.PY_SOURCE)
+ self.load_module(name, fp, pathname, stuff)
+
+ def import_hook(self, name, caller=None, fromlist=None, level=-1):
+ self.msg(3, "import_hook", name, caller, fromlist, level)
+ parent = self.determine_parent(caller, level=level)
+ q, tail = self.find_head_package(parent, name)
+ m = self.load_tail(q, tail)
+ if not fromlist:
+ return q
+ if m.__path__:
+ self.ensure_fromlist(m, fromlist)
+ return None
+
+ def determine_parent(self, caller, level=-1):
+ self.msgin(4, "determine_parent", caller, level)
+ if not caller or level == 0:
+ self.msgout(4, "determine_parent -> None")
+ return None
+ pname = caller.__name__
+ if level >= 1: # relative import
+ if caller.__path__:
+ level -= 1
+ if level == 0:
+ parent = self.modules[pname]
+ assert parent is caller
+ self.msgout(4, "determine_parent ->", parent)
+ return parent
+ if pname.count(".") < level:
+ raise ImportError("relative importpath too deep")
+ pname = ".".join(pname.split(".")[:-level])
+ parent = self.modules[pname]
+ self.msgout(4, "determine_parent ->", parent)
+ return parent
+ if caller.__path__:
+ parent = self.modules[pname]
+ assert caller is parent
+ self.msgout(4, "determine_parent ->", parent)
+ return parent
+ if '.' in pname:
+ i = pname.rfind('.')
+ pname = pname[:i]
+ parent = self.modules[pname]
+ assert parent.__name__ == pname
+ self.msgout(4, "determine_parent ->", parent)
+ return parent
+ self.msgout(4, "determine_parent -> None")
+ return None
+
+ def find_head_package(self, parent, name):
+ self.msgin(4, "find_head_package", parent, name)
+ if '.' in name:
+ i = name.find('.')
+ head = name[:i]
+ tail = name[i+1:]
+ else:
+ head = name
+ tail = ""
+ if parent:
+ qname = "%s.%s" % (parent.__name__, head)
+ else:
+ qname = head
+ q = self.import_module(head, qname, parent)
+ if q:
+ self.msgout(4, "find_head_package ->", (q, tail))
+ return q, tail
+ if parent:
+ qname = head
+ parent = None
+ q = self.import_module(head, qname, parent)
+ if q:
+ self.msgout(4, "find_head_package ->", (q, tail))
+ return q, tail
+ self.msgout(4, "raise ImportError: No module named", qname)
+ raise ImportError("No module named " + qname)
+
+ def load_tail(self, q, tail):
+ self.msgin(4, "load_tail", q, tail)
+ m = q
+ while tail:
+ i = tail.find('.')
+ if i < 0:
+ i = len(tail)
+ head, tail = tail[:i], tail[i+1:]
+ mname = "%s.%s" % (m.__name__, head)
+ m = self.import_module(head, mname, m)
+ if not m:
+ self.msgout(4, "raise ImportError: No module named", mname)
+ raise ImportError("No module named " + mname)
+ self.msgout(4, "load_tail ->", m)
+ return m
+
+ def ensure_fromlist(self, m, fromlist, recursive=0):
+ self.msg(4, "ensure_fromlist", m, fromlist, recursive)
+ for sub in fromlist:
+ if sub == "*":
+ if not recursive:
+ all = self.find_all_submodules(m)
+ if all:
+ self.ensure_fromlist(m, all, 1)
+ elif not hasattr(m, sub):
+ subname = "%s.%s" % (m.__name__, sub)
+ submod = self.import_module(sub, subname, m)
+ if not submod:
+ raise ImportError("No module named " + subname)
+
+ def find_all_submodules(self, m):
+ if not m.__path__:
+ return
+ modules = {}
+ # 'suffixes' used to be a list hardcoded to [".py", ".pyc", ".pyo"].
+ # But we must also collect Python extension modules - although
+ # we cannot separate normal dlls from Python extensions.
+ suffixes = []
+ for triple in imp.get_suffixes():
+ suffixes.append(triple[0])
+ for dir in m.__path__:
+ try:
+ names = os.listdir(dir)
+ except os.error:
+ self.msg(2, "can't list directory", dir)
+ continue
+ for name in names:
+ mod = None
+ for suff in suffixes:
+ n = len(suff)
+ if name[-n:] == suff:
+ mod = name[:-n]
+ break
+ if mod and mod != "__init__":
+ modules[mod] = mod
+ return list(modules.keys())
+
+ def import_module(self, partname, fqname, parent):
+ self.msgin(3, "import_module", partname, fqname, parent)
+ try:
+ m = self.modules[fqname]
+ except KeyError:
+ pass
+ else:
+ self.msgout(3, "import_module ->", m)
+ return m
+ if fqname in self.badmodules:
+ self.msgout(3, "import_module -> None")
+ return None
+ if parent and parent.__path__ is None:
+ self.msgout(3, "import_module -> None")
+ return None
+ try:
+ fp, pathname, stuff = self.find_module(partname,
+ parent and parent.__path__, parent)
+ except ImportError:
+ self.msgout(3, "import_module ->", None)
+ return None
+ try:
+ m = self.load_module(fqname, fp, pathname, stuff)
+ finally:
+ if fp:
+ fp.close()
+ if parent:
+ setattr(parent, partname, m)
+ self.msgout(3, "import_module ->", m)
+ return m
+
+ def load_module(self, fqname, fp, pathname, xxx_todo_changeme):
+ (suffix, mode, type) = xxx_todo_changeme
+ self.msgin(2, "load_module", fqname, fp and "fp", pathname)
+ if type == imp.PKG_DIRECTORY:
+ m = self.load_package(fqname, pathname)
+ self.msgout(2, "load_module ->", m)
+ return m
+ if type == imp.PY_SOURCE:
+ co = compile(fp.read()+'\n', pathname, 'exec')
+ elif type == imp.PY_COMPILED:
+ if fp.read(4) != imp.get_magic():
+ self.msgout(2, "raise ImportError: Bad magic number", pathname)
+ raise ImportError("Bad magic number in %s" % pathname)
+ fp.read(4)
+ co = marshal.load(fp)
+ else:
+ co = None
+ m = self.add_module(fqname)
+ m.__file__ = pathname
+ if co:
+ if self.replace_paths:
+ co = self.replace_paths_in_code(co)
+ m.__code__ = co
+ self.scan_code(co, m)
+ self.msgout(2, "load_module ->", m)
+ return m
+
+ def _add_badmodule(self, name, caller):
+ if name not in self.badmodules:
+ self.badmodules[name] = {}
+ if caller:
+ self.badmodules[name][caller.__name__] = 1
+ else:
+ self.badmodules[name]["-"] = 1
+
+ def _safe_import_hook(self, name, caller, fromlist, level=-1):
+ # wrapper for self.import_hook() that won't raise ImportError
+ if name in self.badmodules:
+ self._add_badmodule(name, caller)
+ return
+ try:
+ self.import_hook(name, caller, level=level)
+ except ImportError as msg:
+ self.msg(2, "ImportError:", str(msg))
+ self._add_badmodule(name, caller)
+ else:
+ if fromlist:
+ for sub in fromlist:
+ if sub in self.badmodules:
+ self._add_badmodule(sub, caller)
+ continue
+ try:
+ self.import_hook(name, caller, [sub], level=level)
+ except ImportError as msg:
+ self.msg(2, "ImportError:", str(msg))
+ fullname = name + "." + sub
+ self._add_badmodule(fullname, caller)
+
+ def scan_opcodes(self, co,
+ unpack=struct.unpack):
+ # Scan the code, and yield 'interesting' opcode combinations
+ # Version for Python 2.4 and older
+ code = co.co_code
+ names = co.co_names
+ consts = co.co_consts
+ while code:
+ c = code[0]
+ if c in STORE_OPS:
+ oparg, = unpack('= HAVE_ARGUMENT:
+ code = code[3:]
+ else:
+ code = code[1:]
+
+ def scan_opcodes_25(self, co,
+ unpack=struct.unpack):
+ # Scan the code, and yield 'interesting' opcode combinations
+ # Python 2.5 version (has absolute and relative imports)
+ code = co.co_code
+ names = co.co_names
+ consts = co.co_consts
+ LOAD_LOAD_AND_IMPORT = LOAD_CONST + LOAD_CONST + IMPORT_NAME
+ while code:
+ c = code[0]
+ if c in STORE_OPS:
+ oparg, = unpack('= HAVE_ARGUMENT:
+ code = code[3:]
+ else:
+ code = code[1:]
+
+ def scan_code(self, co, m):
+ if m.__name__ in self.skip_scan:
+ return
+ code = co.co_code
+ if sys.version_info >= (2, 5):
+ scanner = self.scan_opcodes_25
+ else:
+ scanner = self.scan_opcodes
+ for what, args in scanner(co):
+ if what == "store":
+ name, = args
+ m.globalnames[name] = 1
+ elif what in ("import", "absolute_import"):
+ fromlist, name = args
+ have_star = 0
+ if fromlist is not None:
+ if "*" in fromlist:
+ have_star = 1
+ fromlist = [f for f in fromlist if f != "*"]
+ if what == "absolute_import":
+ level = 0
+ else:
+ level = -1
+ self._safe_import_hook(name, m, fromlist, level=level)
+ if have_star:
+ # We've encountered an "import *". If it is a Python module,
+ # the code has already been parsed and we can suck out the
+ # global names.
+ mm = None
+ if m.__path__:
+ # At this point we don't know whether 'name' is a
+ # submodule of 'm' or a global module. Let's just try
+ # the full name first.
+ mm = self.modules.get(m.__name__ + "." + name)
+ if mm is None:
+ mm = self.modules.get(name)
+ if mm is not None:
+ m.globalnames.update(mm.globalnames)
+ m.starimports.update(mm.starimports)
+ if mm.__code__ is None:
+ m.starimports[name] = 1
+ else:
+ m.starimports[name] = 1
+ elif what == "relative_import":
+ level, fromlist, name = args
+ if name:
+ self._safe_import_hook(name, m, fromlist, level=level)
+ else:
+ parent = self.determine_parent(m, level=level)
+ self._safe_import_hook(
+ parent.__name__, None, fromlist, level=0)
+ else:
+ # We don't expect anything else from the generator.
+ raise RuntimeError(what)
+
+ for c in co.co_consts:
+ if isinstance(c, type(co)):
+ self.scan_code(c, m)
+
+ def load_package(self, fqname, pathname):
+ self.msgin(2, "load_package", fqname, pathname)
+ newname = replacePackageMap.get(fqname)
+ if newname:
+ fqname = newname
+ m = self.add_module(fqname)
+ m.__file__ = pathname
+ m.__path__ = [pathname]
+
+ # As per comment at top of file, simulate runtime __path__ additions.
+ m.__path__ = m.__path__ + packagePathMap.get(fqname, [])
+
+ fp, buf, stuff = self.find_module("__init__", m.__path__)
+ self.load_module(fqname, fp, buf, stuff)
+ self.msgout(2, "load_package ->", m)
+ return m
+
+ def add_module(self, fqname):
+ if fqname in self.modules:
+ return self.modules[fqname]
+ self.modules[fqname] = m = Module(fqname)
+ return m
+
+ def find_module(self, name, path, parent=None):
+ if parent is not None:
+ # assert path is not None
+ fullname = parent.__name__+'.'+name
+ else:
+ fullname = name
+ if fullname in self.excludes:
+ self.msgout(3, "find_module -> Excluded", fullname)
+ raise ImportError(name)
+
+ if path is None:
+ if name in sys.builtin_module_names:
+ return (None, None, ("", "", imp.C_BUILTIN))
+
+ path = self.path
+ return imp.find_module(name, path)
+
+ def report(self):
+ """Print a report to stdout, listing the found modules with their
+ paths, as well as modules that are missing, or seem to be missing.
+ """
+ print()
+ print(" %-25s %s" % ("Name", "File"))
+ print(" %-25s %s" % ("----", "----"))
+ # Print modules found
+ keys = list(self.modules.keys())
+ keys.sort()
+ for key in keys:
+ m = self.modules[key]
+ if m.__path__:
+ print("P", end=' ')
+ else:
+ print("m", end=' ')
+ print("%-25s" % key, m.__file__ or "")
+
+ # Print missing modules
+ missing, maybe = self.any_missing_maybe()
+ if missing:
+ print()
+ print("Missing modules:")
+ for name in missing:
+ mods = list(self.badmodules[name].keys())
+ mods.sort()
+ print("?", name, "imported from", ', '.join(mods))
+ # Print modules that may be missing, but then again, maybe not...
+ if maybe:
+ print()
+ print("Submodules thay appear to be missing, but could also be", end=' ')
+ print("global names in the parent package:")
+ for name in maybe:
+ mods = list(self.badmodules[name].keys())
+ mods.sort()
+ print("?", name, "imported from", ', '.join(mods))
+
+ def any_missing(self):
+ """Return a list of modules that appear to be missing. Use
+ any_missing_maybe() if you want to know which modules are
+ certain to be missing, and which *may* be missing.
+ """
+ missing, maybe = self.any_missing_maybe()
+ return missing + maybe
+
+ def any_missing_maybe(self):
+ """Return two lists, one with modules that are certainly missing
+ and one with modules that *may* be missing. The latter names could
+ either be submodules *or* just global names in the package.
+
+ The reason it can't always be determined is that it's impossible to
+ tell which names are imported when "from module import *" is done
+ with an extension module, short of actually importing it.
+ """
+ missing = []
+ maybe = []
+ for name in self.badmodules:
+ if name in self.excludes:
+ continue
+ i = name.rfind(".")
+ if i < 0:
+ missing.append(name)
+ continue
+ subname = name[i+1:]
+ pkgname = name[:i]
+ pkg = self.modules.get(pkgname)
+ if pkg is not None:
+ if pkgname in self.badmodules[name]:
+ # The package tried to import this module itself and
+ # failed. It's definitely missing.
+ missing.append(name)
+ elif subname in pkg.globalnames:
+ # It's a global in the package: definitely not missing.
+ pass
+ elif pkg.starimports:
+ # It could be missing, but the package did an "import *"
+ # from a non-Python module, so we simply can't be sure.
+ maybe.append(name)
+ else:
+ # It's not a global in the package, the package didn't
+ # do funny star imports, it's very likely to be missing.
+ # The symbol could be inserted into the package from the
+ # outside, but since that's not good style we simply list
+ # it missing.
+ missing.append(name)
+ else:
+ missing.append(name)
+ missing.sort()
+ maybe.sort()
+ return missing, maybe
+
+ def replace_paths_in_code(self, co):
+ new_filename = original_filename = os.path.normpath(co.co_filename)
+ for f, r in self.replace_paths:
+ if original_filename.startswith(f):
+ new_filename = r + original_filename[len(f):]
+ break
+
+ if self.debug and original_filename not in self.processed_paths:
+ if new_filename != original_filename:
+ self.msgout(2, "co_filename %r changed to %r"
+ % (original_filename, new_filename,))
+ else:
+ self.msgout(2, "co_filename %r remains unchanged"
+ % (original_filename,))
+ self.processed_paths.append(original_filename)
+
+ consts = list(co.co_consts)
+ for i in range(len(consts)):
+ if isinstance(consts[i], type(co)):
+ consts[i] = self.replace_paths_in_code(consts[i])
+
+ return types.CodeType(co.co_argcount, co.co_nlocals, co.co_stacksize,
+ co.co_flags, co.co_code, tuple(
+ consts), co.co_names,
+ co.co_varnames, new_filename, co.co_name,
+ co.co_firstlineno, co.co_lnotab,
+ co.co_freevars, co.co_cellvars)
+
+
+def test():
+ # Parse command line
+ import getopt
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "dmp:qx:")
+ except getopt.error as msg:
+ print(msg)
+ return
+
+ # Process options
+ debug = 1
+ domods = 0
+ addpath = []
+ exclude = []
+ for o, a in opts:
+ if o == '-d':
+ debug = debug + 1
+ if o == '-m':
+ domods = 1
+ if o == '-p':
+ addpath = addpath + a.split(os.pathsep)
+ if o == '-q':
+ debug = 0
+ if o == '-x':
+ exclude.append(a)
+
+ # Provide default arguments
+ if not args:
+ script = "hello.py"
+ else:
+ script = args[0]
+
+ # Set the path based on sys.path and the script directory
+ path = sys.path[:]
+ path[0] = os.path.dirname(script)
+ path = addpath + path
+ if debug > 1:
+ print("path:")
+ for item in path:
+ print(" ", repr(item))
+
+ # Create the module finder and turn its crank
+ mf = ModuleFinder(path, debug, exclude)
+ for arg in args[1:]:
+ if arg == '-m':
+ domods = 1
+ continue
+ if domods:
+ if arg[-2:] == '.*':
+ mf.import_hook(arg[:-2], None, ["*"])
+ else:
+ mf.import_hook(arg)
+ else:
+ mf.load_file(arg)
+ mf.run_script(script)
+ mf.report()
+ return mf # for -i debugging
+
+
+if __name__ == '__main__':
+ try:
+ mf = test()
+ except KeyboardInterrupt:
+ print("\n[interrupt]")
+
+
+# py2exe specific portion - this should be removed before inclusion in the
+# Python distribution
+
+
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+Base = ModuleFinder
+del ModuleFinder
+
+# Much inspired by Toby Dickenson's code:
+# http://www.tarind.com/depgraph.html
+
+
+class ModuleFinder(Base):
+ def __init__(self, *args, **kw):
+ self._depgraph = {}
+ self._types = {}
+ self._last_caller = None
+ self._scripts = set()
+ Base.__init__(self, *args, **kw)
+
+ def run_script(self, pathname):
+ # Scripts always end in the __main__ module, but we possibly
+ # have more than one script in py2exe, so we want to keep
+ # *all* the pathnames.
+ self._scripts.add(pathname)
+ Base.run_script(self, pathname)
+
+ def import_hook(self, name, caller=None, fromlist=None, level=-1):
+ old_last_caller = self._last_caller
+ try:
+ self._last_caller = caller
+ return Base.import_hook(self, name, caller, fromlist, level)
+ finally:
+ self._last_caller = old_last_caller
+
+ def import_module(self, partnam, fqname, parent):
+ r = Base.import_module(self, partnam, fqname, parent)
+ if r is not None and self._last_caller:
+ self._depgraph.setdefault(
+ self._last_caller.__name__, set()).add(r.__name__)
+ return r
+
+ def load_module(self, fqname, fp, pathname, xxx_todo_changeme1):
+ (suffix, mode, typ) = xxx_todo_changeme1
+ r = Base.load_module(self, fqname, fp, pathname, (suffix, mode, typ))
+ if r is not None:
+ self._types[r.__name__] = typ
+ return r
+
+ def create_xref(self):
+ # this code probably needs cleanup
+ depgraph = {}
+ importedby = {}
+ for name, value in list(self._depgraph.items()):
+ depgraph[name] = list(value)
+ for needs in value:
+ importedby.setdefault(needs, set()).add(name)
+
+ names = list(self._types.keys())
+ names.sort()
+
+ fd, htmlfile = tempfile.mkstemp(".html")
+ ofi = open(htmlfile, "w")
+ os.close(fd)
+ print("py2exe cross reference for %s" %
+ sys.argv[0], file=ofi)
+
+ print("