diff --git a/README.md b/README.md index a3b795749..5700287f8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ If you wish to help with development or simply need to run pyfa through a Python * `dateutil` * `matplotlib` (for some Linux distributions you may need to install separate wxPython bindings such as `python-matplotlib-wx`) * `requests` +* `logbook` >= 1.0.0 ## Bug Reporting The preferred method of reporting bugs is through the project's [GitHub Issues interface](https://github.com/pyfa-org/Pyfa/issues). Alternatively, posting a report in the [pyfa thread](http://forums.eveonline.com/default.aspx?g=posts&t=247609) on the official EVE Online forums is acceptable. Guidelines for bug reporting can be found on [this wiki page](https://github.com/DarkFenX/Pyfa/wiki/Bug-Reporting). diff --git a/config.py b/config.py index 2c50fd001..13a7d9cb9 100644 --- a/config.py +++ b/config.py @@ -20,7 +20,7 @@ saveInRoot = False # Version data version = "1.28.1" -tag = "git" +tag = "Stable" expansionName = "YC119.3" expansionVersion = "1.0" evemonMinVersion = "4081" diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 3338e8351..b013a5f3f 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -128,9 +128,9 @@ class Fit(object): self.__capRecharge = None self.__calculatedTargets = [] self.__remoteReps = { - "Armor": None, - "Shield": None, - "Hull": None, + "Armor" : None, + "Shield" : None, + "Hull" : None, "Capacitor": None, } self.factorReload = False @@ -370,9 +370,11 @@ class Fit(object): @validates("ID", "ownerID", "shipID") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "ownerID": lambda _val: isinstance(_val, int) or _val is None, - "shipID": lambda _val: isinstance(_val, int) or _val is None} + map = { + "ID" : lambda _val: isinstance(_val, int), + "ownerID": lambda _val: isinstance(_val, int) or _val is None, + "shipID" : lambda _val: isinstance(_val, int) or _val is None + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) @@ -408,15 +410,15 @@ class Fit(object): self.ship.clear() c = chain( - self.modules, - self.drones, - self.fighters, - self.boosters, - self.implants, - self.projectedDrones, - self.projectedModules, - self.projectedFighters, - (self.character, self.extraAttributes), + self.modules, + self.drones, + self.fighters, + self.boosters, + self.implants, + self.projectedDrones, + self.projectedModules, + self.projectedFighters, + (self.character, self.extraAttributes), ) for stuff in c: @@ -476,11 +478,11 @@ class Fit(object): if warfareBuffID == 11: # Shield Burst: Active Shielding: Repair Duration/Capacitor self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( - "Shield Emission Systems"), "capacitorNeed", value) + lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( + "Shield Emission Systems"), "capacitorNeed", value) self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( - "Shield Emission Systems"), "duration", value) + lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( + "Shield Emission Systems"), "duration", value) if warfareBuffID == 12: # Shield Burst: Shield Extension: Shield HP self.ship.boostItemAttr("shieldCapacity", value, stackingPenalties=True) @@ -506,26 +508,26 @@ class Fit(object): if warfareBuffID == 17: # Information Burst: Electronic Superiority: EWAR Range and Strength groups = ("ECM", "Sensor Dampener", "Weapon Disruptor", "Target Painter") self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value, - stackingPenalties=True) + stackingPenalties=True) self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, - "falloffEffectiveness", value, stackingPenalties=True) + "falloffEffectiveness", value, stackingPenalties=True) for scanType in ("Magnetometric", "Radar", "Ladar", "Gravimetric"): self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "ECM", - "scan%sStrengthBonus" % scanType, value, - stackingPenalties=True) + "scan%sStrengthBonus" % scanType, value, + stackingPenalties=True) for attr in ("missileVelocityBonus", "explosionDelayBonus", "aoeVelocityBonus", "falloffBonus", "maxRangeBonus", "aoeCloudSizeBonus", "trackingSpeedBonus"): self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Weapon Disruptor", - attr, value) + attr, value) for attr in ("maxTargetRangeBonus", "scanResolutionBonus"): self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Sensor Dampener", - attr, value) + attr, value) self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Target Painter", - "signatureRadiusBonus", value, stackingPenalties=True) + "signatureRadiusBonus", value, stackingPenalties=True) if warfareBuffID == 18: # Information Burst: Electronic Hardening: Scan Strength for scanType in ("Gravimetric", "Radar", "Ladar", "Magnetometric"): @@ -544,40 +546,36 @@ class Fit(object): if warfareBuffID == 21: # Skirmish Burst: Interdiction Maneuvers: Tackle Range groups = ("Stasis Web", "Warp Scrambler") self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value, - stackingPenalties=True) + stackingPenalties=True) if warfareBuffID == 22: # Skirmish Burst: Rapid Deployment: AB/MWD Speed Increase - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Afterburner") - or mod.item.requiresSkill("High Speed Maneuvering"), - "speedFactor", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Afterburner") or + mod.item.requiresSkill("High Speed Maneuvering"), + "speedFactor", value, stackingPenalties=True) if warfareBuffID == 23: # Mining Burst: Mining Laser Field Enhancement: Mining/Survey Range - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Mining") or - mod.item.requiresSkill("Ice Harvesting") or - mod.item.requiresSkill("Gas Cloud Harvesting"), - "maxRange", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or + mod.item.requiresSkill("Ice Harvesting") or + mod.item.requiresSkill("Gas Cloud Harvesting"), + "maxRange", value, stackingPenalties=True) self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("CPU Management"), - "surveyScanRange", value, stackingPenalties=True) + "surveyScanRange", value, stackingPenalties=True) if warfareBuffID == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Mining") or - mod.item.requiresSkill("Ice Harvesting") or - mod.item.requiresSkill("Gas Cloud Harvesting"), - "capacitorNeed", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or + mod.item.requiresSkill("Ice Harvesting") or + mod.item.requiresSkill("Gas Cloud Harvesting"), + "capacitorNeed", value, stackingPenalties=True) - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Mining") or - mod.item.requiresSkill("Ice Harvesting") or - mod.item.requiresSkill("Gas Cloud Harvesting"), - "duration", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or + mod.item.requiresSkill("Ice Harvesting") or + mod.item.requiresSkill("Gas Cloud Harvesting"), + "duration", value, stackingPenalties=True) if warfareBuffID == 25: # Mining Burst: Mining Equipment Preservation: Crystal Volatility self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining"), - "crystalVolatilityChance", value, stackingPenalties=True) + "crystalVolatilityChance", value, stackingPenalties=True) if warfareBuffID == 60: # Skirmish Burst: Evasive Maneuvers: Agility self.ship.boostItemAttr("agility", value, stackingPenalties=True) @@ -850,15 +848,17 @@ class Fit(object): return amount - slots = {Slot.LOW: "lowSlots", - Slot.MED: "medSlots", - Slot.HIGH: "hiSlots", - Slot.RIG: "rigSlots", - Slot.SUBSYSTEM: "maxSubSystems", - Slot.SERVICE: "serviceSlots", - Slot.F_LIGHT: "fighterLightSlots", - Slot.F_SUPPORT: "fighterSupportSlots", - Slot.F_HEAVY: "fighterHeavySlots"} + slots = { + Slot.LOW : "lowSlots", + Slot.MED : "medSlots", + Slot.HIGH : "hiSlots", + Slot.RIG : "rigSlots", + Slot.SUBSYSTEM: "maxSubSystems", + Slot.SERVICE : "serviceSlots", + Slot.F_LIGHT : "fighterLightSlots", + Slot.F_SUPPORT: "fighterSupportSlots", + Slot.F_HEAVY : "fighterHeavySlots" + } def getSlotsFree(self, type, countDummies=False): if type in (Slot.MODE, Slot.SYSTEM): @@ -1007,29 +1007,35 @@ class Fit(object): def calculateSustainableTank(self, effective=True): if self.__sustainableTank is None: if self.capStable: - sustainable = {"armorRepair": self.extraAttributes["armorRepair"], - "shieldRepair": self.extraAttributes["shieldRepair"], - "hullRepair": self.extraAttributes["hullRepair"]} + sustainable = { + "armorRepair" : self.extraAttributes["armorRepair"], + "shieldRepair": self.extraAttributes["shieldRepair"], + "hullRepair" : self.extraAttributes["hullRepair"] + } else: sustainable = {} repairers = [] # Map a repairer type to the attribute it uses - groupAttrMap = {"Armor Repair Unit": "armorDamageAmount", - "Ancillary Armor Repairer": "armorDamageAmount", - "Hull Repair Unit": "structureDamageAmount", - "Shield Booster": "shieldBonus", - "Ancillary Shield Booster": "shieldBonus", - "Remote Armor Repairer": "armorDamageAmount", - "Remote Shield Booster": "shieldBonus"} + groupAttrMap = { + "Armor Repair Unit" : "armorDamageAmount", + "Ancillary Armor Repairer": "armorDamageAmount", + "Hull Repair Unit" : "structureDamageAmount", + "Shield Booster" : "shieldBonus", + "Ancillary Shield Booster": "shieldBonus", + "Remote Armor Repairer" : "armorDamageAmount", + "Remote Shield Booster" : "shieldBonus" + } # Map repairer type to attribute - groupStoreMap = {"Armor Repair Unit": "armorRepair", - "Hull Repair Unit": "hullRepair", - "Shield Booster": "shieldRepair", - "Ancillary Shield Booster": "shieldRepair", - "Remote Armor Repairer": "armorRepair", - "Remote Shield Booster": "shieldRepair", - "Ancillary Armor Repairer": "armorRepair", } + groupStoreMap = { + "Armor Repair Unit" : "armorRepair", + "Hull Repair Unit" : "hullRepair", + "Shield Booster" : "shieldRepair", + "Ancillary Shield Booster": "shieldRepair", + "Remote Armor Repairer" : "armorRepair", + "Remote Shield Booster" : "shieldRepair", + "Ancillary Armor Repairer": "armorRepair", + } capUsed = self.capUsed for attr in ("shieldRepair", "armorRepair", "hullRepair"): @@ -1057,7 +1063,7 @@ class Fit(object): # Sort repairers by efficiency. We want to use the most efficient repairers first repairers.sort(key=lambda _mod: _mod.getModifiedItemAttr( - groupAttrMap[_mod.item.group.name]) / _mod.getModifiedItemAttr("capacitorNeed"), reverse=True) + groupAttrMap[_mod.item.group.name]) / _mod.getModifiedItemAttr("capacitorNeed"), reverse=True) # Loop through every module until we're above peak recharge # Most efficient first, as we sorted earlier. @@ -1370,10 +1376,10 @@ class Fit(object): def __repr__(self): return u"Fit(ID={}, ship={}, name={}) at {}".format( - self.ID, self.ship.item.name, self.name, hex(id(self)) + self.ID, self.ship.item.name, self.name, hex(id(self)) ).encode('utf8') def __str__(self): return u"{} ({})".format( - self.name, self.ship.item.name + self.name, self.ship.item.name ).encode('utf8') diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index bac688769..8287f349a 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -173,7 +173,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): charges = 0 else: charges = floor(containerCapacity / chargeVolume) - return charges + return int(charges) @property def numShots(self): diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index bb8631623..e69de29bb 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -1,27 +0,0 @@ -__all__ = [ - "openFit", - # "moduleGlobalAmmoPicker", - "moduleAmmoPicker", - "itemStats", - "damagePattern", - "marketJump", - "droneSplit", - "itemRemove", - "droneRemoveStack", - "ammoPattern", - "project", - "factorReload", - "whProjector", - "cargo", - "shipJump", - "changeAffectingSkills", - "tacticalMode", - "targetResists", - "priceClear", - "amount", - "metaSwap", - "implantSets", - "fighterAbilities", - "cargoAmmo", - "droneStack" -] diff --git a/gui/builtinContextMenus/cargoAmmo.py b/gui/builtinContextMenus/cargoAmmo.py index 573e4c24f..57ae42517 100644 --- a/gui/builtinContextMenus/cargoAmmo.py +++ b/gui/builtinContextMenus/cargoAmmo.py @@ -3,13 +3,19 @@ import gui.mainFrame import service import gui.globalEvents as GE import wx +from service.settings import ContextMenuSettings +from service.fit import Fit class CargoAmmo(ContextMenu): def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = ContextMenuSettings.getInstance() def display(self, srcContext, selection): + if not self.settings.get('cargoAmmo'): + return False + if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None: return False @@ -23,7 +29,7 @@ class CargoAmmo(ContextMenu): return "Add {0} to Cargo (x1000)".format(itmContext) def activate(self, fullContext, selection, i): - sFit = service.Fit.getInstance() + sFit = Fit.getInstance() fitID = self.mainFrame.getActiveFit() typeID = int(selection[0].ID) diff --git a/gui/builtinContextMenus/droneStack.py b/gui/builtinContextMenus/droneStack.py index b7fde1040..150b76676 100644 --- a/gui/builtinContextMenus/droneStack.py +++ b/gui/builtinContextMenus/droneStack.py @@ -3,13 +3,19 @@ import gui.mainFrame import service import gui.globalEvents as GE import wx +from service.settings import ContextMenuSettings +from service.fit import Fit -class CargoAmmo(ContextMenu): +class DroneStack(ContextMenu): def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = ContextMenuSettings.getInstance() def display(self, srcContext, selection): + if not self.settings.get('droneStack'): + return False + if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None: return False @@ -25,7 +31,7 @@ class CargoAmmo(ContextMenu): return "Add {0} to Drone Bay (x5)".format(itmContext) def activate(self, fullContext, selection, i): - sFit = service.Fit.getInstance() + sFit = Fit.getInstance() fitID = self.mainFrame.getActiveFit() typeID = int(selection[0].ID) @@ -34,4 +40,4 @@ class CargoAmmo(ContextMenu): wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) -CargoAmmo.register() +DroneStack.register() diff --git a/gui/builtinStatsViews/outgoingViewMinimal.py b/gui/builtinStatsViews/outgoingViewMinimal.py index 70f7994dd..d2bf2c118 100644 --- a/gui/builtinStatsViews/outgoingViewMinimal.py +++ b/gui/builtinStatsViews/outgoingViewMinimal.py @@ -20,7 +20,6 @@ # noinspection PyPackageRequirements import wx from gui.statsView import StatsView -from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount diff --git a/gui/contextMenu.py b/gui/contextMenu.py index 243a4a743..a35c73282 100644 --- a/gui/contextMenu.py +++ b/gui/contextMenu.py @@ -181,7 +181,7 @@ class ContextMenu(object): # noinspection PyUnresolvedReferences from gui.builtinContextMenus import ( # noqa: E402,F401 openFit, - # moduleGlobalAmmoPicker, + moduleGlobalAmmoPicker, moduleAmmoPicker, itemStats, damagePattern, @@ -200,6 +200,8 @@ from gui.builtinContextMenus import ( # noqa: E402,F401 targetResists, priceClear, amount, + cargoAmmo, + droneStack, metaSwap, implantSets, fighterAbilities, diff --git a/gui/graphFrame.py b/gui/graphFrame.py index 7a0aa3387..2abeadbab 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame.py @@ -29,13 +29,14 @@ import gui.mainFrame import gui.globalEvents as GE from gui.graph import Graph from gui.bitmapLoader import BitmapLoader +import traceback pyfalog = Logger(__name__) try: import matplotlib as mpl - mpl_version = int(mpl.__version__[0]) + mpl_version = int(mpl.__version__[0]) or -1 if mpl_version >= 2: mpl.use('wxagg') mplImported = True @@ -48,43 +49,33 @@ try: graphFrame_enabled = True mplImported = True -except ImportError: +except ImportError as e: + pyfalog.warning("Matplotlib failed to import. Likely missing or incompatible version.") + mpl_version = -1 + Patch = mpl = Canvas = Figure = None + graphFrame_enabled = False + mplImported = False +except Exception: + # We can get exceptions deep within matplotlib. Catch those. See GH #1046 + tb = traceback.format_exc() + pyfalog.critical("Exception when importing Matplotlib. Continuing without importing.") + pyfalog.critical(tb) + mpl_version = -1 Patch = mpl = Canvas = Figure = None graphFrame_enabled = False mplImported = False -pyfalog = Logger(__name__) - class GraphFrame(wx.Frame): def __init__(self, parent, style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE | wx.FRAME_FLOAT_ON_PARENT): - global graphFrame_enabled global mplImported - - self.Patch = None - self.mpl_version = -1 - - try: - import matplotlib as mpl - self.mpl_version = int(mpl.__version__[0]) - if self.mpl_version >= 2: - mpl.use('wxagg') - mplImported = True - else: - mplImported = False - from matplotlib.patches import Patch - self.Patch = Patch - from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas - from matplotlib.figure import Figure - graphFrame_enabled = True - except ImportError: - Patch = mpl = Canvas = Figure = None - graphFrame_enabled = False + global mpl_version self.legendFix = False + if not graphFrame_enabled: - pyfalog.info("Problems importing matplotlib; continuing without graphs") + pyfalog.warning("Matplotlib is not enabled. Skipping initialization.") return try: @@ -236,6 +227,8 @@ class GraphFrame(wx.Frame): self.draw() def draw(self, event=None): + global mpl_version + values = self.getValues() view = self.getView() self.subplot.clear() @@ -260,7 +253,7 @@ class GraphFrame(wx.Frame): self.canvas.draw() return - if self.mpl_version < 2: + if mpl_version < 2: if self.legendFix and len(legend) > 0: leg = self.subplot.legend(tuple(legend), "upper right", shadow=False) for t in leg.get_texts(): @@ -276,7 +269,7 @@ class GraphFrame(wx.Frame): for l in leg.get_lines(): l.set_linewidth(1) - elif self.mpl_version >= 2: + elif mpl_version >= 2: legend2 = [] legend_colors = { 0: "blue", diff --git a/pyfa.py b/pyfa.py index 488165724..06f8632d1 100755 --- a/pyfa.py +++ b/pyfa.py @@ -26,6 +26,7 @@ import config from optparse import OptionParser, BadOptionError, AmbiguousOptionError +import logbook from logbook import TimedRotatingFileHandler, Logger, StreamHandler, NestedSetup, FingersCrossedHandler, NullHandler, \ CRITICAL, ERROR, WARNING, DEBUG, INFO pyfalog = Logger(__name__) @@ -144,6 +145,10 @@ if not hasattr(sys, 'frozen'): print("Cannot find python-dateutil.\nYou can download python-dateutil from https://pypi.python.org/pypi/python-dateutil") sys.exit(1) + logVersion = logbook.__version__.split('.') + if int(logVersion[0]) < 1: + print ("Logbook version >= 1.0.0 is recommended. You may have some performance issues by continuing to use an earlier version.") + if __name__ == "__main__": # Configure paths diff --git a/requirements.txt b/requirements.txt index ed301fe25..d3bc33dde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -logbook +logbook>=1.0.0 matplotlib PyYAML python-dateutil diff --git a/service/settings.py b/service/settings.py index 5d44d4e88..cfc48fc4a 100644 --- a/service/settings.py +++ b/service/settings.py @@ -408,14 +408,17 @@ class ContextMenuSettings(object): "ammoPattern" : 1, "amount" : 1, "cargo" : 1, + "cargoAmmo" : 1, "changeAffectingSkills" : 1, "damagePattern" : 1, "droneRemoveStack" : 1, "droneSplit" : 1, + "droneStack" : 1, "factorReload" : 1, "fighterAbilities" : 1, - "implantSet" : 1, + "implantSets" : 1, "itemStats" : 1, + "itemRemove" : 1, "marketJump" : 1, "metaSwap" : 1, "moduleAmmoPicker" : 1,