From 95e4ad82b4cefbe757cefa195625b524cc39c323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 13 Jan 2017 11:25:08 +0100 Subject: [PATCH 1/9] Cura: Fix off-by-one error for placeholder replacement Affected print_temperature and filament_diameter placeholders. E.g. `print_temperature2` was wrongly attempting to fetch `print_temperatures[2]` instead of `print_temperatures[1]` (0-based-indexing!) Partially fixes #1708 --- src/octoprint/plugins/cura/profile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/octoprint/plugins/cura/profile.py b/src/octoprint/plugins/cura/profile.py index e8783d12..5f8538bd 100644 --- a/src/octoprint/plugins/cura/profile.py +++ b/src/octoprint/plugins/cura/profile.py @@ -602,8 +602,8 @@ class Profile(object): diameters = self._get("filament_diameter") if not match.group(1): return diameters[0] - index = int(match.group(1)) - if index >= len(diameters): + index = int(match.group(1)) - 1 + if index >= len(diameters) or index < 0: return 0.0 return diameters[index] @@ -615,8 +615,8 @@ class Profile(object): temperatures = self._get("print_temperature") if not match.group(1): return temperatures[0] - index = int(match.group(1)) - if index >= len(temperatures): + index = int(match.group(1)) - 1 + if index >= len(temperatures) or index < 0: return 0.0 return temperatures[index] From 20cd13904dffcf608694375d2ece8156a6991a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 13 Jan 2017 12:54:01 +0100 Subject: [PATCH 2/9] Cura: Fix selection of which start/end gcode to choose So far the plugin selected the gcode script corresponding to the number of extruders configured in the printer profile. After a close look into the implementation in Cura itself, prompted by #1708, it turns out that this is in fact wrong. Cura selects the gcode script to use based on the maximum of the number of extruders needed for printing the models in the scene and the minimum number of extruders needed for generating support (if support extruder is "both" or "first" or there is only one extruder, that's 1, if it's "second" and there is more than one extruder, it's 2). This commit changes the plugin's implementation to mirror this implementation. The difference to Cura is that we have the number of extruders needed for the models in the scene hard coded to 1 since we only support STL right now which can never contain more than one object. If we ever decide to support merging of multiple STLs into one single multi-extruder print or other model files like AMF or OBJ from OctoPrint's slicer support, we need to change this. --- src/octoprint/plugins/cura/__init__.py | 34 +++++++++++++++++++------- src/octoprint/plugins/cura/profile.py | 21 ++++++++-------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index 3633cf5e..058445d6 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -246,11 +246,11 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, machinecode_path = path + ".gco" if position and isinstance(position, dict) and "x" in position and "y" in position: - posX = position["x"] - posY = position["y"] + pos_x = position["x"] + pos_y = position["y"] else: - posX = None - posY = None + pos_x = None + pos_y = None if on_progress: if not on_progress_args: @@ -266,8 +266,24 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, working_dir = os.path.dirname(executable) - slicing_profile = Profile(self._load_profile(profile_path), printer_profile, posX, posY) - engine_settings = self._convert_to_engine(profile_path, printer_profile, posX, posY) + slicing_profile = Profile(self._load_profile(profile_path), printer_profile, pos_x, pos_y) + + # NOTE: We can assume an extruder count of 1 here since the only way we currently + # support dual extrusion in this implementation is by using the second extruder for support (which + # the engine conversion will automatically detect and adapt accordingly). + # + # We currently do only support STL files as sliceables, which by default can only contain one mesh, + # so no risk of having to slice multi-objects at the moment, which would necessitate a full analysis + # of the objects to slice to determine amount of needed extruders to use here. If we ever decide to + # also support dual extrusion slicing (including composition from multiple STLs or support for OBJ or + # AMF files and the like), this code needs to be adapted! + # + # The extruder count is needed to decide which start/end gcode will be used from the Cura profile. + # Stock Cura implementation counts the number of objects in the scene for this (and also takes a look + # at the support usage, like the engine conversion here does). We only ever have one object. + engine_settings = self._convert_to_engine(profile_path, printer_profile, + pos_x=pos_x, pos_y=pos_y, + used_extruders=1) # Start building the argument list for the CuraEngine command execution args = [executable, '-v', '-p'] @@ -443,9 +459,9 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, with octoprint.util.atomic_write(path, "wb", max_permissions=0o666) as f: yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True) - def _convert_to_engine(self, profile_path, printer_profile, posX, posY): - profile = Profile(self._load_profile(profile_path), printer_profile, posX, posY) - return profile.convert_to_engine() + def _convert_to_engine(self, profile_path, printer_profile, pos_x=None, pos_y=None, used_extruders=1): + profile = Profile(self._load_profile(profile_path), printer_profile, pos_x, pos_y) + return profile.convert_to_engine(used_extruders=used_extruders) def _sanitize_name(name): if name is None: diff --git a/src/octoprint/plugins/cura/profile.py b/src/octoprint/plugins/cura/profile.py index 5f8538bd..2f8792d0 100644 --- a/src/octoprint/plugins/cura/profile.py +++ b/src/octoprint/plugins/cura/profile.py @@ -674,9 +674,7 @@ class Profile(object): return default return int(value * 1000) - def get_gcode_template(self, key): - extruder_count = self.get_int("extruder_amount") - + def get_gcode_template(self, key, extruder_count=1): if key in self._profile: gcode = self._profile[key] else: @@ -736,7 +734,7 @@ class Profile(object): return pre + str(f) - def get_gcode(self, key): + def get_gcode(self, key, extruder_count=1): prefix = "" postfix = "" @@ -746,11 +744,11 @@ class Profile(object): return "" if key == "start_gcode": - contents = self.get_gcode_template("start_gcode") + contents = self.get_gcode_template("start_gcode", extruder_count=extruder_count) prefix += self.get_start_gcode_prefix(contents) else: - contents = self.get_gcode_template(key) + contents = self.get_gcode_template(key, extruder_count=extruder_count) return unicode(prefix + re.sub("(.)\{([^\}]*)\}", self.replaceTagMatch, contents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n' @@ -861,13 +859,14 @@ class Profile(object): return int(self.get_float("machine_depth") / 2.0) if not self.get_boolean("machine_center_is_zero") else 0.0 - def convert_to_engine(self): + def convert_to_engine(self, used_extruders=1): edge_width, line_count = self.calculate_edge_width_and_line_count() solid_layer_count = self.calculate_solid_layer_count() extruder_count = self.get_int("extruder_amount") minimal_extruder_count = self.calculate_minimal_extruder_count() + actual_extruder_count = max(minimal_extruder_count, used_extruders) settings = { "layerThickness": self.get_microns("layer_height"), @@ -916,10 +915,10 @@ class Profile(object): "posy": self.get_pos_y() * 1000, # in microns # gcodes - "startCode": self.get_gcode("start_gcode"), - "endCode": self.get_gcode("end_gcode"), - "preSwitchExtruderCode": self.get_gcode("preSwitchExtruder_gcode"), - "postSwitchExtruderCode": self.get_gcode("postSwitchExtruder_gcode"), + "startCode": self.get_gcode("start_gcode", extruder_count=actual_extruder_count), + "endCode": self.get_gcode("end_gcode", extruder_count=actual_extruder_count), + "preSwitchExtruderCode": self.get_gcode("preSwitchExtruder_gcode", extruder_count=actual_extruder_count), + "postSwitchExtruderCode": self.get_gcode("postSwitchExtruder_gcode", extruder_count=actual_extruder_count), # fixing "fixHorrible": 0, From b1057a4cc8c2b6cedda79ba8bde1ae1d286cdb04 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Wed, 11 Jan 2017 23:22:21 +0100 Subject: [PATCH 3/9] Simplified install process on Mac (cherry picked from commit 726eec8) --- THIRDPARTYLICENSES.md | 1 + docs/development/index.rst | 1 - setup.py | 3 +++ src/octoprint/settings.py | 8 ++------ 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/THIRDPARTYLICENSES.md b/THIRDPARTYLICENSES.md index 381e182a..249f4420 100644 --- a/THIRDPARTYLICENSES.md +++ b/THIRDPARTYLICENSES.md @@ -55,3 +55,4 @@ * [SockJS-Tornado](http://github.com/mrjoes/sockjs-tornado/): MIT * [Tornado](http://www.tornadoweb.org/): Apache License 2.0 * [watchdog](http://github.com/gorakhargosh/watchdog): Apache License 2.0 + * [appdirs](http://github.com/ActiveState/appdirs): MIT diff --git a/docs/development/index.rst b/docs/development/index.rst index 585561c8..7d24a843 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -145,7 +145,6 @@ You'll need a user account with administrator privileges. cd OctoPrint virtualenv venv source venv/bin/activate - pip install -U pyobjc pip install -e .[develop] You can then start OctoPrint via ``~/devel/OctoPrint/venv/bin/octoprint`` or just ``octoprint`` if you activated the virtual diff --git a/setup.py b/setup.py index 55dbd85a..71b81f0d 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,9 @@ INSTALL_REQUIRES = [ "websocket-client>=0.40,<0.41" ] +if sys.platform == "darwin": + INSTALL_REQUIRES.append("appdirs>=1.4.0") + # Additional requirements for optional install options EXTRA_REQUIRES = dict( # Dependencies for developing OctoPrint diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index fa50ba19..bfa29ade 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -1490,12 +1490,8 @@ class Settings(object): def _default_basedir(applicationName): # taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python if sys.platform == "darwin": - from AppKit import NSSearchPathForDirectoriesInDomains - # http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains - # NSApplicationSupportDirectory = 14 - # NSUserDomainMask = 1 - # True for expanding the tilde into a fully qualified path - return os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, True)[0], applicationName) + from appdirs import * + return user_data_dir(applicationName, "") elif sys.platform == "win32": return os.path.join(os.environ["APPDATA"], applicationName) else: From c194fdbb28a84c4295d21fe295bcb788889fe56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 13 Jan 2017 13:30:38 +0100 Subject: [PATCH 4/9] Added @noahsmartin to AUTHORS --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index a85ff3f2..986237fd 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -74,6 +74,7 @@ date of first contribution): * [Mathias Rangel Wulff](https://github.com/mathiasrw) * [Clemens Niemeyer](https://github.com/clemniem) * ["I-am-me"](https://github.com/I-am-me) + * [Noah Martin](https://github.com/noahsmartin) OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by [Daid Braam](https://github.com/daid). Parts of its communication layer and From cb89509bb2060e1927b05a29afc1f5e543420c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 13 Jan 2017 13:30:57 +0100 Subject: [PATCH 5/9] Fixed sorting in 3rd party licenses --- THIRDPARTYLICENSES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/THIRDPARTYLICENSES.md b/THIRDPARTYLICENSES.md index 249f4420..e8818d6c 100644 --- a/THIRDPARTYLICENSES.md +++ b/THIRDPARTYLICENSES.md @@ -32,6 +32,7 @@ ## Server + * [appdirs](http://github.com/ActiveState/appdirs): MIT * [Awesome-Slugify](https://pypi.python.org/pypi/awesome-slugify): GPLv3 * [Click](http://click.pocoo.org/): BSD * [feedparser](https://github.com/kurtmckee/feedparser): BSD @@ -55,4 +56,3 @@ * [SockJS-Tornado](http://github.com/mrjoes/sockjs-tornado/): MIT * [Tornado](http://www.tornadoweb.org/): Apache License 2.0 * [watchdog](http://github.com/gorakhargosh/watchdog): Apache License 2.0 - * [appdirs](http://github.com/ActiveState/appdirs): MIT From 74597483dc59e1e959a2308e501c7a095a5c35b8 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Thu, 12 Jan 2017 02:59:47 +0100 Subject: [PATCH 6/9] Added a mask icon for Safari pinned tab and touchbar (cherry picked from commit ff02192) --- src/octoprint/static/img/favicon.svg | 67 ++++++++++++++++++++++++++++ src/octoprint/templates/index.jinja2 | 1 + 2 files changed, 68 insertions(+) create mode 100644 src/octoprint/static/img/favicon.svg diff --git a/src/octoprint/static/img/favicon.svg b/src/octoprint/static/img/favicon.svg new file mode 100644 index 00000000..a16a1a32 --- /dev/null +++ b/src/octoprint/static/img/favicon.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index 09196c3a..e4c9108b 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -4,6 +4,7 @@ OctoPrint + From 1f0e67f85ea1e8e0ad9c0901d76cb456ae408933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 13 Jan 2017 13:42:53 +0100 Subject: [PATCH 7/9] Added FAQ link to footer --- src/octoprint/templates/index.jinja2 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index e4c9108b..59b49542 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -120,6 +120,7 @@
  • {{ _('Sourcecode') }}
  • {{ _('Documentation') }}
  • {{ _('Bugs and Requests') }}
  • +
  • {{ _('FAQ') }}
  • From 5b3a7bed9376b2107f22b0ab5c36658275a1d25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 13 Jan 2017 13:56:50 +0100 Subject: [PATCH 8/9] Preparing release of 1.3.1rc1 --- CHANGELOG.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ SUPPORTERS.md | 8 ++------ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3587919c..8ed198f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,57 @@ # OctoPrint Changelog +## 1.3.1rc1 (2017-01-13) + +### Note for upgraders + +#### Change in stock terminal filter configuration + +1.3.1 fixes an issue with the two terminal filters for suppressing temperature and SD status messages and adds a new filter for filtering out firmware `wait` messages. These changes will only be active automatically though for stock terminal filter configurations. If you have customized your terminal filters, you'll need to apply these changes manually under "Settings > Terminal filters": + + * Changed "Suppress temperature messages" filter, new regex is `(Send: (N\d+\s+)?M105)|(Recv: ok (B|T\d*):)` + * Changed "Suppress SD status messages" filter, new regex is `(Send: (N\d+\s+)?M27)|(Recv: SD printing byte)` + * New "Suppress wait responses" filter, regex is `Recv: wait` + +### Improvements + + * [#1607](https://github.com/foosel/OctoPrint/issues/1607) - Way better support for password managers (e.g. browser built-in, 1Password, Lastpass) + * [#1638](https://github.com/foosel/OctoPrint/issues/1638) - Make confirmation dialog when cancelling a print optional. + * [#1656](https://github.com/foosel/OctoPrint/issues/1656) - Make wording of buttons on print cancel dialog less confusing. + * [#1705](https://github.com/foosel/OctoPrint/pull/1705) - Simplified install process on Mac by removing dependency on pyobjc. + * [#1706](https://github.com/foosel/OctoPrint/pull/1706) - Added a mask icon for Safari pinned tab and touchbar. + * Support extraction of filament diameter for volume calculation from GCODE files sliced through Simplify3D. + * Abort low priority jobs in the file analysis queue when a high priority job comes in - should make file analysis and hence time estimates show up faster for newly uploaded files. + * Added a terminal filter for firmware `wait` messages to the stock terminal filters. If you did modify your terminal filter configuration, you might want to add this manually: + * New "Suppress wait responses" filter: `Recv: wait` + +### Bug fixes + + * [#1637](https://github.com/foosel/OctoPrint/issues/1637) - Fix issue preventing a folder to be deleted that has a name which is a common prefix of the file currently being printed. + * [#1641](https://github.com/foosel/OctoPrint/issues/1641) - Fix issue with `octoprint --daemon` not working. + * [#1647](https://github.com/foosel/OctoPrint/issues/1647) - Fix issue with `octoprint` command throwing an error if an environment variable `OCTOPRINT_VERSION` was set to its version number. + * [#1648](https://github.com/foosel/OctoPrint/issues/1648) - Added missing `websocket-client` dependency of `octoprint client` to install script. + * [#1653](https://github.com/foosel/OctoPrint/issues/1653) - Fix for an issue with the included init script on the BBB (see also [#1654](https://github.com/foosel/OctoPrint/issues/1654)) + * [#1657](https://github.com/foosel/OctoPrint/issues/1657) - Fix init script regarding check for configured `CONFIGFILE` variable. + * [#1657](https://github.com/foosel/OctoPrint/issues/1657) - Don't care about ordering of common parameters (like `--basedir`, `--config`) on CLI. + * [#1660](https://github.com/foosel/OctoPrint/issues/1660) - Do not show hint regarding keyboard controls beneath webcam stream if keyboard control feature is disabled. + * [#1667](https://github.com/foosel/OctoPrint/issues/1667) - Fix for matching folders not getting listed in the results when performing a search in the file list. + * [#1675](https://github.com/foosel/OctoPrint/issues/1675) - Fix model size calculation in GCODE analysis, produced wrong values in some cases. Also adjusted calculation to match implementation in GCODE viewer, now both produce identical results. + * [#1685](https://github.com/foosel/OctoPrint/issues/1685) - Cura Plugin: Fix filament extraction from CuraEngine slicing output + * [#1692](https://github.com/foosel/OctoPrint/issues/1692) - Cura Plugin: Fix solid layer calculation (backport from [Ultimaker/CuraEngine#140](https://github.com/Ultimaker/CuraEngine/issues/140)) + * [#1693](https://github.com/foosel/OctoPrint/issues/1693) - Cura Plugin: Support `perimeter_before_infill` profile setting. Additionally added support for `solidarea_speed`, `raft_airgap_all`, `raft_surface_thickness`, `raft_surface_linewidth` profile settings and adjusted mapping for engine settings `raftAirGapLayer0`, `raftFanSpeed`, `raftSurfaceThickness`and `raftSurfaceLinewidth` according to current mapping in Cura Legacy and adjusted Mach3 GCODE flavor to substitute `S` with `P` in temperature commands of generated start code, also like in Cura Legacy. + * [#1697](https://github.com/foosel/OctoPrint/issues/1697) - Pin Jinja to versions <2.9 for now due to a backwards compatibility issue with templates for versions newer than that. Also pushed as a hotfix to 1.3.0 (as 1.3.0post1). + * Allow a retraction z-hop of 0 in timelapse configuration. + * Fix files in sub folders to not be processed by the initial analysis backlog check during startup of the server. + * Various fixes in the file analysis queue: + * High priority items are now really high priority + * Abort analysis for items that are to be deleted/moved to get around an issue with file access under Windows systems. + * Fix stock terminal filters for suppressing temperature messages and SD status messages to also be able to deal with line number prefixes. If you have added additional terminal filters, you will have to apply this fix manually: + * Changed "Suppress temperature messages" filter: `(Send: (N\d+\s+)?M105)|(Recv: ok (B|T\d*):)` + * Changed "Suppress SD status messages" filter: `(Send: (N\d+\s+)?M27)|(Recv: SD printing byte)` + * Fix issue in german translation. + +([Commits](https://github.com/foosel/OctoPrint/compare/1.3.0...1.3.1rc1)) + ## 1.3.0 (2016-12-08) ### Features diff --git a/SUPPORTERS.md b/SUPPORTERS.md index f4160f8f..8cb932d8 100644 --- a/SUPPORTERS.md +++ b/SUPPORTERS.md @@ -11,12 +11,10 @@ thanks to everyone who contributed! * Andrew Moorby * Arnljot Arntsen * Aurelio Bernal - * Bart Zudell * Boris Hussein * Brad Jackson * Brent Fiegle * Brian E. Tyler - * Charles Mitchell * Christopher Day * Christian Petropolis * COLLE+McVOY @@ -25,7 +23,6 @@ thanks to everyone who contributed! * DeltaMaker 3D Printers * Doug Johnson * E3D BigBox - * Erik de Bruijn * Ernesto Martinez * Exovite * Frank Sander @@ -34,7 +31,6 @@ thanks to everyone who contributed! * George Robles * J. Eckert * Jamie van Dyke - * Jason Galarneau * Josh Daniels * Joshua David Gregory * Kaile Riser @@ -59,7 +55,6 @@ thanks to everyone who contributed! * Samer Najia * SD3D * SeeMeCNC - * Shane Ekerbicer * Simon Hallam * Stefan Krister * Stephane Schittly @@ -67,6 +62,7 @@ thanks to everyone who contributed! * Sven Mueller * Terrance Shaw * Thomas Hatley + * Timeshell.ca * Trent Shumay -and 972 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)! +and 1016 more wonderful people pledging on the [Patreon campaign](https://patreon.com/foosel)! From 4c971a92db5bd724583da9e5c2cd4f670dd40d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 13 Jan 2017 14:17:39 +0100 Subject: [PATCH 9/9] from ... import * is only allowed at module level --- src/octoprint/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index bfa29ade..e52bac7a 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -1490,8 +1490,8 @@ class Settings(object): def _default_basedir(applicationName): # taken from http://stackoverflow.com/questions/1084697/how-do-i-store-desktop-application-data-in-a-cross-platform-way-for-python if sys.platform == "darwin": - from appdirs import * - return user_data_dir(applicationName, "") + import appdirs + return appdirs.user_data_dir(applicationName, "") elif sys.platform == "win32": return os.path.join(os.environ["APPDATA"], applicationName) else: