Merge branch 'master' into development

Conflicts:
	gui/builtinViewColumns/baseName.py
This commit is contained in:
Ryan Holmes
2017-02-08 13:58:37 -05:00
16 changed files with 216 additions and 47 deletions

View File

@@ -85,8 +85,7 @@ class DatabaseCleanup:
logger.error("More than one uniform damage pattern found.")
else:
uniform_damage_pattern_id = rows[0]['ID']
update_query = "UPDATE 'fits' SET 'damagePatternID' = " + str(uniform_damage_pattern_id) + \
" WHERE damagePatternID NOT IN (SELECT ID FROM damagePatterns) OR damagePatternID IS NULL"
update_query = "UPDATE 'fits' SET 'damagePatternID' = {} WHERE damagePatternID NOT IN (SELECT ID FROM damagePatterns) OR damagePatternID IS NULL".format(uniform_damage_pattern_id)
update_results = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, update_query)
logger.error("Database corruption found. Cleaning up %d records.", update_results.rowcount)
@@ -160,3 +159,75 @@ class DatabaseCleanup:
query = "DELETE FROM targetResists WHERE name IS NULL OR name = ''"
delete = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
logger.error("Database corruption found. Cleaning up %d records.", delete.rowcount)
@staticmethod
def OrphanedFitIDItemID(saveddata_engine):
# Orphaned items that are missing the fit ID or item ID.
# See issue #954
for table in ['drones', 'cargo', 'fighters']:
logger.debug("Running database cleanup for orphaned %s items.", table)
query = "SELECT COUNT(*) AS num FROM {} WHERE itemID IS NULL OR itemID = '' or itemID = '0' or fitID IS NULL OR fitID = '' or fitID = '0'".format(table)
results = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
if results is None:
return
row = results.first()
if row and row['num']:
query = "DELETE FROM {} WHERE itemID IS NULL OR itemID = '' or itemID = '0' or fitID IS NULL OR fitID = '' or fitID = '0'".format(table)
delete = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
logger.error("Database corruption found. Cleaning up %d records.", delete.rowcount)
for table in ['modules']:
logger.debug("Running database cleanup for orphaned %s items.", table)
query = "SELECT COUNT(*) AS num FROM {} WHERE itemID = '0' or fitID IS NULL OR fitID = '' or fitID = '0'".format(table)
results = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
if results is None:
return
row = results.first()
if row and row['num']:
query = "DELETE FROM {} WHERE itemID = '0' or fitID IS NULL OR fitID = '' or fitID = '0'".format(table)
delete = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
logger.error("Database corruption found. Cleaning up %d records.", delete.rowcount)
@staticmethod
def NullDamageTargetPatternValues(saveddata_engine):
# Find patterns that have null values
# See issue #954
for profileType in ['damagePatterns', 'targetResists']:
for damageType in ['em', 'thermal', 'kinetic', 'explosive']:
logger.debug("Running database cleanup for null %s values. (%s)", profileType, damageType)
query = "SELECT COUNT(*) AS num FROM {0} WHERE {1}Amount IS NULL OR {1}Amount = ''".format(profileType, damageType)
results = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
if results is None:
return
row = results.first()
if row and row['num']:
query = "UPDATE '{0}' SET '{1}Amount' = '0' WHERE {1}Amount IS NULL OR Amount = ''".format(profileType, damageType)
delete = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
logger.error("Database corruption found. Cleaning up %d records.", delete.rowcount)
@staticmethod
def DuplicateSelectedAmmoName(saveddata_engine):
# Orphaned items that are missing the fit ID or item ID.
# See issue #954
logger.debug("Running database cleanup for duplicated selected ammo profiles.")
query = "SELECT COUNT(*) AS num FROM damagePatterns WHERE name = 'Selected Ammo'"
results = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
if results is None:
return
row = results.first()
if row and row['num'] > 1:
query = "DELETE FROM damagePatterns WHERE name = 'Selected Ammo'"
delete = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query)
logger.error("Database corruption found. Cleaning up %d records.", delete.rowcount)

View File

@@ -15,8 +15,6 @@ def handler(fit, module, context):
# Skip if there is no damage pattern. Example: projected ships or fleet boosters
if damagePattern:
# logger.debug("Damage Pattern: %f/%f/%f/%f", damagePattern.emAmount, damagePattern.thermalAmount, damagePattern.kineticAmount, damagePattern.explosiveAmount)
# logger.debug("Original Armor Resists: %f/%f/%f/%f", fit.ship.getModifiedItemAttr('armorEmDamageResonance'), fit.ship.getModifiedItemAttr('armorThermalDamageResonance'), fit.ship.getModifiedItemAttr('armorKineticDamageResonance'), fit.ship.getModifiedItemAttr('armorExplosiveDamageResonance'))
# Populate a tuple with the damage profile modified by current armor resists.
baseDamageTaken = (
@@ -50,7 +48,6 @@ def handler(fit, module, context):
(2, baseDamageTaken[2] * RAHResistance[2], RAHResistance[2]),
(1, baseDamageTaken[1] * RAHResistance[1], RAHResistance[1]),
]
# logger.debug("Damage taken this cycle: %f/%f/%f/%f", damagePattern_tuples[0][1], damagePattern_tuples[3][1], damagePattern_tuples[2][1], damagePattern_tuples[1][1])
# Sort the tuple to drop the highest damage value to the bottom
sortedDamagePattern_tuples = sorted(damagePattern_tuples, key=lambda damagePattern: damagePattern[1])

View File

@@ -3,4 +3,7 @@ type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Information Command Specialist"), "commandBonusHidden", src.getModifiedItemAttr("eliteBonusCommandDestroyer1"), skill="Command Destroyers")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Information Command Specialist"),
"commandBonusHidden",
src.getModifiedItemAttr("eliteBonusCommandDestroyer1"),
skill="Command Destroyers")

View File

@@ -6,5 +6,11 @@ type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), "falloffEffectiveness", src.getModifiedItemAttr("eliteBonusLogistics1"), skill="Logistics Cruisers")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), "maxRange", src.getModifiedItemAttr("eliteBonusLogistics1"), skill="Logistics Cruisers")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"),
"falloffEffectiveness",
src.getModifiedItemAttr("eliteBonusLogistics1"),
skill="Logistics Cruisers")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"),
"maxRange",
src.getModifiedItemAttr("eliteBonusLogistics1"),
skill="Logistics Cruisers")

View File

@@ -6,5 +6,9 @@ type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), "falloffEffectiveness", src.getModifiedItemAttr("roleBonusRepairRange"))
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), "maxRange", src.getModifiedItemAttr("roleBonusRepairRange"))
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"),
"falloffEffectiveness",
src.getModifiedItemAttr("roleBonusRepairRange"))
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"),
"maxRange",
src.getModifiedItemAttr("roleBonusRepairRange"))

View File

@@ -6,8 +6,18 @@ type = "passive"
def handler(fit, src, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or mod.item.requiresSkill("Information Command"), "warfareBuff4Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or mod.item.requiresSkill("Information Command"), "warfareBuff3Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or mod.item.requiresSkill("Information Command"), "warfareBuff1Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or mod.item.requiresSkill("Information Command"), "buffDuration", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or mod.item.requiresSkill("Information Command"), "warfareBuff2Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or
mod.item.requiresSkill("Information Command"),
"warfareBuff4Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or
mod.item.requiresSkill("Information Command"),
"warfareBuff3Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or
mod.item.requiresSkill("Information Command"),
"warfareBuff1Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or
mod.item.requiresSkill("Information Command"),
"buffDuration", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Armored Command") or
mod.item.requiresSkill("Information Command"),
"warfareBuff2Value", src.getModifiedItemAttr("shipBonusForceAuxiliaryA4"), skill="Amarr Carrier")

View File

@@ -243,9 +243,11 @@ class Item(EqBase):
return self.__attributes
def getAttribute(self, key):
def getAttribute(self, key, default=None):
if key in self.attributes:
return self.attributes[key].value
else:
return default
def isType(self, type):
for effect in self.effects.itervalues():

View File

@@ -25,19 +25,19 @@ cappingAttrKeyCache = {}
class ItemAttrShortcut(object):
def getModifiedItemAttr(self, key):
def getModifiedItemAttr(self, key, default=None):
if key in self.itemModifiedAttributes:
return self.itemModifiedAttributes[key]
else:
return None
return default
class ChargeAttrShortcut(object):
def getModifiedChargeAttr(self, key):
def getModifiedChargeAttr(self, key, default=None):
if key in self.chargeModifiedAttributes:
return self.chargeModifiedAttributes[key]
else:
return None
return default
class ModifiedAttributeDict(collections.MutableMapping):

View File

@@ -489,10 +489,12 @@ class Fit(object):
self.ship.boostItemAttr("armor%sDamageResonance" % damageType, value)
if warfareBuffID == 14: # Armor Burst: Rapid Repair: Repair Duration/Capacitor
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or mod.item.requiresSkill("Repair Systems"),
"capacitorNeed", value)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or mod.item.requiresSkill("Repair Systems"), "duration",
value)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems"),
"capacitorNeed", value)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems") or
mod.item.requiresSkill("Repair Systems"),
"duration", value)
if warfareBuffID == 15: # Armor Burst: Armor Reinforcement: Armor HP
self.ship.boostItemAttr("armorHP", value, stackingPenalties=True)
@@ -626,7 +628,8 @@ class Fit(object):
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Shield Emission Systems"), "shieldBonus", value, stackingPenalties=True)
if warfareBuffID == 53: # Leviathan Effect Generator : Armor RR penalty
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), "armorDamageAmount", value, stackingPenalties=True)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"),
"armorDamageAmount", value, stackingPenalties=True)
if warfareBuffID == 54: # Ragnarok Effect Generator : Laser and Hybrid Optimal penalty
groups = ("Energy Weapon", "Hybrid Weapon")

View File

@@ -24,7 +24,8 @@ from eos.saveddata.implant import Implant
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
from eos.saveddata.module import Module, Slot, Rack
from service.fit import Fit
from eos.saveddata.fit import Fit
from service.fit import Fit as FitSvc
from gui.viewColumn import ViewColumn
import gui.mainFrame
@@ -57,7 +58,7 @@ class BaseName(ViewColumn):
else:
return "%s (%s)" % (stuff.name, stuff.ship.item.name)
elif isinstance(stuff, Rack):
if Fit.getInstance().serviceFittingOptions["rackLabels"]:
if FitSvc.getInstance().serviceFittingOptions["rackLabels"]:
if stuff.slot == Slot.MODE:
return u'─ Tactical Mode ─'
else:
@@ -74,7 +75,7 @@ class BaseName(ViewColumn):
else:
item = getattr(stuff, "item", stuff)
if Fit.getInstance().serviceFittingOptions["showMarketShortcuts"]:
if FitSvc.getInstance().serviceFittingOptions["showMarketShortcuts"]:
marketShortcut = getattr(item, "marketShortcut", None)
if marketShortcut:

View File

@@ -450,30 +450,69 @@ class Miscellanea(ViewColumn):
return text, item.name
else:
return "", None
elif itemGroup in ("Ancillary Armor Repairer", "Ancillary Shield Booster"):
hp = stuff.hpBeforeReload
cycles = stuff.numShots
cycleTime = stuff.rawCycleTime
elif itemGroup in (
"Ancillary Armor Repairer",
"Ancillary Shield Booster",
"Capacitor Booster",
"Ancillary Remote Armor Repairer",
"Ancillary Remote Shield Booster",
):
if "Armor" in itemGroup or "Shield" in itemGroup:
boosted_attribute = "HP"
reload_time = item.getAttribute("reloadTime", 0) / 1000
elif "Capacitor" in itemGroup:
boosted_attribute = "Cap"
reload_time = 10
else:
boosted_attribute = ""
reload_time = 0
cycles = max(stuff.numShots, 0)
cycleTime = max(stuff.rawCycleTime, 0)
# Get HP or boosted amount
stuff_hp = max(stuff.hpBeforeReload, 0)
armor_hp = stuff.getModifiedItemAttr("armorDamageAmount", 0)
capacitor_hp = stuff.getModifiedChargeAttr("capacitorBonus", 0)
shield_hp = stuff.getModifiedItemAttr("shieldBonus", 0)
hp = max(stuff_hp, armor_hp * cycles, capacitor_hp * cycles, shield_hp * cycles, 0)
if not hp or not cycleTime or not cycles:
return "", None
fit = Fit.getInstance().getFit(self.mainFrame.getActiveFit())
ehpTotal = fit.ehp
hpTotal = fit.hp
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
tooltip = "HP restored over duration using charges"
if useEhp:
if itemGroup == "Ancillary Armor Repairer":
tooltip = "{0} restored over duration using charges (plus reload)".format(boosted_attribute)
if useEhp and boosted_attribute == "HP" and "Remote" not in itemGroup:
if "Ancillary Armor Repairer" in itemGroup:
hpRatio = ehpTotal["armor"] / hpTotal["armor"]
else:
hpRatio = ehpTotal["shield"] / hpTotal["shield"]
tooltip = "E{0}".format(tooltip)
else:
hpRatio = 1
if itemGroup == "Ancillary Armor Repairer":
hpRatio *= 3
if "Ancillary" in itemGroup and "Armor" in itemGroup:
hpRatio *= stuff.getModifiedItemAttr("chargedArmorDamageMultiplier", 1)
ehp = hp * hpRatio
duration = cycles * cycleTime / 1000
text = "{0} / {1}s".format(formatAmount(ehp, 3, 0, 9), formatAmount(duration, 3, 0, 3))
for number_of_cycles in {5, 10, 25}:
tooltip = "{0}\n{1} charges lasts {2} seconds ({3} cycles)".format(
tooltip,
formatAmount(number_of_cycles*cycles, 3, 0, 3),
formatAmount((duration+reload_time)*number_of_cycles, 3, 0, 3),
formatAmount(number_of_cycles, 3, 0, 3)
)
text = "{0} / {1}s (+{2}s)".format(
formatAmount(ehp, 3, 0, 9),
formatAmount(duration, 3, 0, 3),
formatAmount(reload_time, 3, 0, 3)
)
return text, tooltip
elif itemGroup == "Armor Resistance Shift Hardener":

View File

@@ -26,6 +26,7 @@ import wx
import time
from codecs import open
from wx._core import PyDeadObjectError
from wx.lib.wordwrap import wordwrap
@@ -70,6 +71,9 @@ from service.port import Port
from service.settings import HTMLExportSettings
from time import gmtime, strftime
import logging
logger = logging.getLogger(__name__)
import threading
import webbrowser
@@ -349,6 +353,13 @@ class MainFrame(wx.Frame):
info = wx.AboutDialogInfo()
info.Name = "pyfa"
info.Version = gui.aboutData.versionString
try:
import matplotlib
matplotlib_version = matplotlib.__version__
except:
matplotlib_version = None
info.Description = wordwrap(gui.aboutData.description + "\n\nDevelopers:\n\t" +
"\n\t".join(gui.aboutData.developers) +
"\n\nAdditional credits:\n\t" +
@@ -358,7 +369,8 @@ class MainFrame(wx.Frame):
"\n\nEVE Data: \t" + gamedata_version +
"\nPython: \t\t" + '{}.{}.{}'.format(v.major, v.minor, v.micro) +
"\nwxPython: \t" + wx.__version__ +
"\nSQLAlchemy: \t" + sqlalchemy.__version__,
"\nSQLAlchemy: \t" + sqlalchemy.__version__ +
"\nmatplotlib: \t {}".format(matplotlib_version if matplotlib_version else "Not Installed"),
500, wx.ClientDC(self))
if "__WXGTK__" in wx.PlatformInfo:
forumUrl = "http://forums.eveonline.com/default.aspx?g=posts&t=466425"
@@ -381,7 +393,10 @@ class MainFrame(wx.Frame):
def showDamagePatternEditor(self, event):
dlg = DmgPatternEditorDlg(self)
dlg.ShowModal()
dlg.Destroy()
try:
dlg.Destroy()
except PyDeadObjectError:
logger.error("Tried to destroy an object that doesn't exist in <showDamagePatternEditor>.")
def showImplantSetEditor(self, event):
ImplantSetEditorDlg(self)
@@ -406,14 +421,20 @@ class MainFrame(wx.Frame):
path += ".xml"
else:
print("oops, invalid fit format %d" % format_)
dlg.Destroy()
try:
dlg.Destroy()
except PyDeadObjectError:
logger.error("Tried to destroy an object that doesn't exist in <showExportDialog>.")
return
with open(path, "w", encoding="utf-8") as openfile:
openfile.write(output)
openfile.close()
dlg.Destroy()
try:
dlg.Destroy()
except PyDeadObjectError:
logger.error("Tried to destroy an object that doesn't exist in <showExportDialog>.")
def showPreferenceDialog(self, event):
dlg = PreferenceDialog(self)
@@ -724,7 +745,10 @@ class MainFrame(wx.Frame):
CopySelectDict[selected]()
dlg.Destroy()
try:
dlg.Destroy()
except PyDeadObjectError:
logger.error("Tried to destroy an object that doesn't exist in <exportToClipboard>.")
def exportSkillsNeeded(self, event):
""" Exports skills needed for active fit and active character """
@@ -778,7 +802,10 @@ class MainFrame(wx.Frame):
self.progressDialog.message = None
sPort.importFitsThreaded(dlg.GetPaths(), self.fileImportCallback)
self.progressDialog.ShowModal()
dlg.Destroy()
try:
dlg.Destroy()
except PyDeadObjectError:
logger.error("Tried to destroy an object that doesn't exist in <fileImportDialog>.")
def backupToXml(self, event):
""" Back up all fits to EVE XML file """

View File

@@ -230,7 +230,7 @@ class exportHtmlThread(threading.Thread):
HTML += (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" data-corners="false">\n'
' <h2>' + group.groupName + ' <span class="ui-li-count">' + str(groupFits) + '</span></h2>\n'
' <ul data-role="listview" data-shadow="false" data-inset="true" data-corners="false">\n' + HTMLgroup +
' <ul data-role="listview" data-shadow="false" data-inset="true" data-corners="false">\n' + HTMLgroup +
' </ul>\n'
' </li>'
)

View File

@@ -82,7 +82,8 @@ if not hasattr(sys, 'frozen'):
betaFlag = True if saMatch.group(3) == "b" else False
saBuild = int(saMatch.group(4)) if not betaFlag else 0
if saMajor == 0 and (saMinor < 5 or (saMinor == 5 and saBuild < 8)):
print("Pyfa requires sqlalchemy 0.5.8 at least but current sqlalchemy version is %s\nYou can download sqlalchemy (0.5.8+) from http://www.sqlalchemy.org/".format(sqlalchemy.__version__))
print("Pyfa requires sqlalchemy 0.5.8 at least but current sqlalchemy version is %s\n"
"You can download sqlalchemy (0.5.8+) from http://www.sqlalchemy.org/".format(sqlalchemy.__version__))
sys.exit(1)
else:
print("Unknown sqlalchemy version string format, skipping check")

View File

@@ -56,6 +56,9 @@ if config.saveDB and os.path.isfile(config.saveDB):
database_cleanup_instance.OrphanedFitDamagePatterns(db.saveddata_engine)
database_cleanup_instance.NullDamagePatternNames(db.saveddata_engine)
database_cleanup_instance.NullTargetResistNames(db.saveddata_engine)
database_cleanup_instance.OrphanedFitIDItemID(db.saveddata_engine)
database_cleanup_instance.NullDamageTargetPatternValues(db.saveddata_engine)
database_cleanup_instance.DuplicateSelectedAmmoName(db.saveddata_engine)
logging.debug("Completed database validation.")
else:

View File

@@ -12,4 +12,6 @@ commands = py.test -vv --cov Pyfa tests/
[testenv:pep8]
deps = flake8
commands = flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py --ignore=E126,E127,E128,E501,E731,F401,F403,F405 service gui eos utils config.py pyfa.py --max-line-length=130
# TODO: Remove F class exceptions once all imports are fixed
# TODO: Remove E731 and convert lambdas to defs
commands = flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py --ignore=E126,E127,E128,E731,F401,F403,F405 service gui eos utils config.py pyfa.py --max-line-length=165