diff --git a/.travis.yml b/.travis.yml index df4eb121f..dd3b081eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,7 @@ python: install: - pip install -e .[develop] script: -- nosetests tests/ \ No newline at end of file +- nosetests --with-doctest +sudo: false +git: + depth: 250 diff --git a/.versioneer-lookup b/.versioneer-lookup index 41fd3a301..b0de673da 100644 --- a/.versioneer-lookup +++ b/.versioneer-lookup @@ -10,13 +10,14 @@ # master shall not use the lookup table, only tags master -# maintenance is currently the branch for preparation of maintenance release 1.2.7 -# so are any fix/... branches -maintenance 1.2.7-dev 536bb31965db17b969e7c1c53e241ddac4ae1814 -fix/.* 1.2.7-dev 536bb31965db17b969e7c1c53e241ddac4ae1814 - -# Special case disconnected checkouts, e.g. 'git checkout ' +# neither should disconnected checkouts, e.g. 'git checkout ' +HEAD \(detached.* +# maintenance is currently the branch for preparation of maintenance release 1.2.8 +# so are any fix/... branches +maintenance 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev +fix/.* 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev + # every other branch is a development branch and thus gets resolved to 1.3.0-dev for now -.* 1.3.0-dev 198d3450d94be1a2 +.* 1.3.0 198d3450d94be1a2 pep440-dev diff --git a/AUTHORS.md b/AUTHORS.md index 0309fdf53..44e77f485 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -53,6 +53,8 @@ date of first contribution): * [Andrew Erickson](https://github.com/aerickson) * [Nicanor Romero Venier](https://github.com/nicanor-romero) * [Thomas Hou](https://github.com/masterhou) + * [Mark Bastiaans](https://github.com/markbastiaans) + * [Kevin Murphy](https://github.com/kevingelion) 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index be40008fe..a07b2b19c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,128 @@ # OctoPrint Changelog +## 1.2.8 (2015-12-07) + +### Notes for Upgraders + +#### A bug in 1.2.7 prevents directly updating to 1.2.8, here's what to do + +A bug in OctoPrint 1.2.7 (fixed in 1.2.8) prevents updating OctoPrint to version +1.2.8. If you try to perform the update, you will simply be told that "the update +was successful", but the update won't actually have taken place. To solve this +hen-egg-problem, a plugin has been made available that fixes said bug (through +monkey patching). + +The plugin is called "Updatefix 1.2.7" and can be found +[in the plugin repository](http://plugins.octoprint.org/plugins/updatefix127/) +and [on Github](https://github.com/OctoPrint/OctoPrint-Updatefix-1.2.7/). + +Before attempting to update your installation from version 1.2.7 to version 1.2.8, +please install the plugin via your plugin manager and restart your server. Note that +you will only see it in the Plugin Manager if you need it, since it's only compatible with +OctoPrint version 1.2.7. After you installed the plugin and restarted your server +you can update as usual. The plugin will self-uninstall once it detects that it's +running under OctoPrint 1.2.8. After the self-uninstall another restart of your server +will be triggered (if you have setup your server's restart command, defaults to +`sudo service octoprint restart` on OctoPi) in order to really get rid of any +left-overs, so don't be alarmed when that happens, it is intentional. + +**If you cannot or don't want to use the plugin**, alternatively you can switch +OctoPrint to "Commit" based tracking via the settings of the Software Update plugin, +update, then switch back to "Release" based tracking (see [this screenshot](https://i.imgur.com/wvkgiGJ.png)). + +#### Bed temperatures are now only displayed if printer profile has a heated bed configured + +This release fixes a [bug](https://github.com/foosel/OctoPrint/issues/1125) +that caused bed temperature display and controls to be available even if the +selected printer profile didn't have a heated bed configured. + +If your printer does have a heated bed but you are not seeing its temperature +in the "Temperature" tab after updating to 1.2.8, please make sure to check +the "Heated Bed" option in your printer profile (under Settings > Printer Profiles) +as shown [in this short GIF](http://i.imgur.com/wp1j9bs.gif). + +### Improvements + + * Version numbering now follows [PEP440](https://www.python.org/dev/peps/pep-0440/). + * Prepared some things for publishing OctoPrint on [PyPi](https://pypi.python.org/pypi) + in the future. + * [BlueprintPlugin mixin](http://docs.octoprint.org/en/master/plugins/mixins.html#blueprintplugin) + now has an `errorhandler` decorator that serves the same purpose as + [Flask's](http://flask.pocoo.org/docs/0.10/patterns/errorpages/#error-handlers) + ([#1059](https://github.com/foosel/OctoPrint/pull/1059)) + * Interpret `M25` in a GCODE file that is being streamed from OctoPrint as + indication to pause, like `M0` and `M1`. + * Cache rendered page and translation files indefinitely. That should + significantly improve performance on reloads of the web interface. + * Added the string "unknown command" to the list of ignored printer errors. + This should help with general firmware compatibility in case a firmware + lacks features. + * Added the strings "cannot open" and "cannot enter" to the list of ignored + printer errors. Those are errors that Marlin may report if there is an issue + with the printer's SD card. + * The "CuraEngine" plugin now makes it more obvious that it only targets + CuraEngine versions up to and including 15.04 and also links to the plugin's + homepage with more information right within the settings dialog. + * Browser tab visibility is now tracked by the web interface, disabling the + webcam and the GCODE viewer if the tab containing OctoPrint is not active. + That should reduce the amount of resource utilized by the web interface on + the client when it is not actively monitored. Might also help to mitigate + [#1065](https://github.com/foosel/OctoPrint/issues/1065), the final verdict + on that one is still out though. + * The printer log in the terminal tab will now be cut off after 3000 lines + even if autoscroll is disabled. If the limit is reached, no more log lines + will be added to the client's buffer. That ensures that the log will not + scroll and the current log excerpt will stay put while also not causing + the browser to run into memory errors due to trying to buffer an endless + amount of log lines. + * Increased timeout of "waiting for restart" after an update from 20 to 60sec + (20sec turned out to be too little for OctoPi for whatever reason). + * Added a couple of unit tests + +### Bug Fixes + + * [#1120](https://github.com/foosel/OctoPrint/issues/1120) - Made the watchdog + that monitors and handles the `watched` folder more resilient towards errors. + * [#1125](https://github.com/foosel/OctoPrint/issues/1125) - Fixed OctoPrint + displaying bed temperature and controls and allowing the sending of GCODE + commands targeting the bed (`M140`, `M190`) if the printer profile doesn't + have a heated bed configured. + * Fixed an issue that stopped the software updater working for OctoPrint. The + updater reports success updating, but no update has actually taken place. A + fix can be applied for this issue to OctoPrint version 1.2.7 via + [the Updatefix 1.2.7 plugin](https://github.com/OctoPrint/OctoPrint-Updatefix-1.2.7). + For more information please refer to the [Important information for people updating from version 1.2.7](#important-information-for-people-updating-from-version-127) + above. + * Fix: Current filename in job data should never be prefixed with `/` + * Only persist plugin settings that differ from the defaults. This way the + `config.yaml` won't be filled with lots of redundant data. It's the + responsibility of the plugin authors to responsibly handle changes in default + settings of their plugins and add data migration where necessary. + * Fixed a documentation bug ([#1067](https://github.com/foosel/OctoPrint/pull/1067)) + * Fixed a conflict with bootstrap-responsive, e.g. when using the + [ScreenSquish Plugin](http://plugins.octoprint.org/plugins/screensquish/) + ([#1103](https://github.com/foosel/OctoPrint/pull/1067)) + * Fixed OctoPrint still sending SD card related commands to the printer even + if SD card support is disabled (e.g. `M21`). + * Hidden files are no longer visible to the template engine, neither as (GCODE) + scripts nor as interface templates. + * The hostname and URL prefix via which the OctoPrint web interface is accessed + is now part of the cache key. Without that being the case the cache could + be created referring to something like `/octoprint/prefix/api/` for its API + endpoint (if accessed via `http://somehost:someport/octoprint/prefix/` first + time), which would then cause the interface to not work if accessed later + via another route (e.g. `http://someotherhost/`). + * Fixed a JavaScript error on finishing streaming of a file to SD. + * Fixed version reporting on detached HEADs (when the branch detection + reported "HEAD" instead of "(detached" + * Fixed some path checks for systems with symlinked paths + ([#1051](https://github.com/foosel/OctoPrint/pull/1051)) + * Fixed a bug causing the "Server Offline" overlay to pop _under_ the + "Please reload" overlay, which could lead to "Connection refused" browser + messages when clicking "Reload now" in the wrong moment. + +([Commits](https://github.com/foosel/OctoPrint/compare/1.2.7...1.2.8)) + ## 1.2.7 (2015-10-20) ### Improvements @@ -386,7 +509,7 @@ changed under "Temperatures" in the Settings ([#343](https://github.com/foosel/OctoPrint/issues/343)). * High-DPI support for the GCode viewer ([#837](https://github.com/foosel/OctoPrint/issues/837)). * Stop websocket connections from multiplying ([#888](https://github.com/foosel/OctoPrint/pull/888)). -* New setting to rotate webcam by 90° counter clockwise ([#895](https://github.com/foosel/OctoPrint/issues/895) and +* New setting to rotate webcam by 90° counter clockwise ([#895](https://github.com/foosel/OctoPrint/issues/895) and [#906](https://github.com/foosel/OctoPrint/pull/906)) * System commands now be set to a) run asynchronized by setting their `async` property to `true` and b) to ignore their result by setting their `ignore` property to `true`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24bc38902..68c09bcb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,51 +1,264 @@ -Issues, Tickets, however you may call them ------------------------------------------- - -Read the following short instructions **fully** and **follow them** if you want your ticket to be taken care of and not closed again directly! They are linked on top of every new issue form, so don't say nobody warned you afterwards. - -- **Read the [FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)** -- Always create **one ticket for one purpose**. So don't mix two or more feature requests, support requests, bugs etc into one ticket. If you do, your ticket will be closed! -- If you want to report a bug, **READ AND FOLLOW [How to file a bug report](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)!** Tickets will be automatically checked if they comply with the requirements outlined in that wiki node! Other then what's written in there (**and really EVERYTHING that is written in there!**) you don't have to do anything special with your ticket. Listen to what GitIssueBot might have to say to you! -- If you want to post a **request** of any kind (feature request, documentation request, ...), **add [Request] to your issue's title!** -- If you need **support** with a problem of your installation (e.g. if you have problems getting the webcam to work) or have a general **question**, the issue tracker is not the right place. Consult the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741) instead! -- If you are a developer that wants to brainstorm a pull request or possible changes to the plugin system, **add [Brainstorming] to your issue's title!** (see below). -- If you have another reason for creating a ticket that doesn't fit any of the above categories, it's something better suited for the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741). - -Following these guidelines (**especially EVERYTHING mentioned in ["How to file a bug report"](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)**) is necessary so the tickets stay manageable - you are not the only one with an open issue, so please respect that you have to **play by the rules** so that your problem can be taken care of. Tickets not playing by the rules **will be closed without further investigation!**. - -Pull Requests -------------- - -1. If you want to add a new feature to OctoPrint, **please always first consider if it wouldn't be better suited for a - plugin.** As a general rule of thumb, any feature that is only of interest to a small sub group should be moved into a - plugin. If the current plugin system doesn't allow you to implement your feature as a plugin, create a "Brainstorming" - ticket to get the discussion going how best to solve *this* in OctoPrint's plugin system - maybe that's the actual PR - you have been waiting for to contribute :) -2. If you plan to make **any large changes to the code or appearance, please open a "Brainstorming" ticket first** so that - we can determine if it's a good time for your specific pull request. It might be that I'm currently in the process of - making heavy changes to the code locations you'd target as well, or your approach doesn't fit the general "project - vision", and that would just cause unnecessary work and frustration for everyone or possibly get the PR rejected. -3. When adding code to OctoPrint, make sure you **follow the current coding style**. That means tabs instead of spaces in the - python files (yes, I know that this goes against PEP-8, I don't care) and space instead of tabs in the Javascript sources, - english language (that means code, variables, comments!), comments where necessary (tell why the code does something like - it does it, structure your code), following the general architecture. If your PR needs to make changes to the Stylesheets, - change the ``.less`` files from which the CSS is compiled. PRs that contain direct changes to the compiled - CSS will be closed. -4. **Test your changes thoroughly**. That also means testing with usage scenarios you don't normally use, e.g. if you only - use access control, test without and vice versa. If you only test with your printer, test with the virtual printer and - vice versa. State in your pull request how your tested your changes. -5. Please create all pull requests **against the `devel` branch**. -6. Create **one pull request per feature/bug fix**. -7. Create a **custom branch** for your feature/bug fix and use that as base for your pull request. Pull requests directly - against your version of `devel` will be closed. -8. In your pull request's description, **state what your pull request is doing**, as in, what feature does it implement, what - bug does it fix. The more thoroughly you explain your intent behind the PR here, the higher the chances it will get merged - fast. -9. Don't forget to **add yourself to the [AUTHORS](../AUTHORS.md) file** :) - -History -------- - - * 2015-01-23: More guidelines for creating pull requests, support/questions redirected to Mailinglist/G+ community +# Contribution Guidelines + +This document outlines what you need to know before **[creating tickets](#issues-tickets-however-you-may-call-them)** +or **[creating pull requests](#pull-requests)**. + +## Contents + + * [Issues, Tickets, however you may call them](#issues-tickets-however-you-may-call-them) + * [How to file a bug report](#how-to-file-a-bug-report) + * [What should I do before submitting a bug report?](#what-should-i-do-before-submitting-a-bug-report) + * [What should I include in a bug report?](#what-should-i-include-in-a-bug-report) + * [Where can I find which version and branch I'm on?](#where-can-i-find-which-version-and-branch-im-on) + * [Where can I find those log files you keep talking about?](#where-can-i-find-those-log-files-you-keep-talking-about) + * [Where can I find my browser's error console?](#where-can-i-find-my-browsers-error-console) + * [Pull requests](#pull-requests) + * [History](#history) + * [Footnotes](#footnotes) + +## Issues, Tickets, however you may call them + +Please read the following short instructions fully and follow them. You can +help the project tremendously this way: not only do you help the maintainers +to **address problems in a timely manner** but also keep it possible for them +to **fix bugs, add new and improve on existing functionality** instead of doing +nothing but ticket management. + +![Ticket flow chart](http://i.imgur.com/qYSZyuw.png) + +- **[Read the FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)** +- If you want to report a **bug**, [read "How to file a bug report" below](#how-to-file-a-bug-report) + and *[use the provided template](#what-should-i-include-in-a-ticket)*. + You do not need to do anything else with your ticket. +- If you want to post a **request** of any kind (feature request, documentation + request, ...), add `[Request]` to your issue's title (e.g. `[Request] Awesome new feature`). +- If you are a **developer** that wants to brainstorm a pull request or possible + changes to the plugin system, add [Brainstorming] to your issue's title (e.g. + `[Brainstorming] New plugin hook for doing some cool stuff`). +- If you need **support**, have a **question** or some **other reason** that + doesn't fit any of the above categories, the issue tracker is not the right place. + Consult the [Mailinglist](https://groups.google.com/group/octoprint) or the + [Google+ Community](https://plus.google.com/communities/102771308349328485741) instead. + +No matter what kind of ticket you create, never mix two or more "ticket reasons" +into one ticket: One ticket per bug, request, brainstorming thread please. + +---- + +**Note**: A bot is in place that monitors new tickets, automatically +categorizes them and checks new bug reports for usage of the provided template. +That bot will only bother you if you open a ticket that appears to be a bug (no +`[Request]` or `[Brainstorming]` in the title) without the template, and it +will do that only to ensure that all information needed to solve the issue is +available for the maintainers to directly start tackling that problem. + +---- + +## How to file a bug report + +If you encounter an issue with OctoPrint, you are welcome to +[submit a bug report](https://goo.gl/GzkGv9). + +Before you do that for the first time though please take a moment to read the +following section *completely*. Thank you! :) + +### What should I do before submitting a bug report? + +1. **Make sure you are at the right location**. This is the Github repository + of the official version of OctoPrint, which is the 3D print server and + corresponding web interface itself. + + **This is not the Github respository of OctoPi**, which is the preconfigured + Raspberry Pi image including OctoPrint among other things - that one can be found + [here](https://github.com/guysoft/OctoPi). Please note that while we do have + some entries regarding OctoPi in the FAQ, any bugs should be reported in the + [proper bug tracker](https://github.com/guysoft/OctoPi/issues) which - again - + is not here. + + **This is also not the Github repository of any OctoPrint Plugins you + might have installed**. Report any issues with those in their corresponding + bug tracker (probably linked to from the plugin's homepage). + + Finally, **this is also not the right issue tracker if you are running an + forked version of OctoPrint**. Seek help for such unofficial versions from + the people maintaining them instead. + +2. Please make sure to **test out the current version** of OctoPrint to see + whether the problem you are encountering still exists. + + If you are feeling up to it you might also want to try the current development + version of OctoPrint (if you aren't already). Refer to the [FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ) + for information on how to do this. + +3. The problem still exists? Then please **look through the + [existing tickets](https://github.com/foosel/OctoPrint/issues?state=open) + (use the [search](https://github.com/foosel/OctoPrint/search?q=&ref=cmdform&type=Issues))** + to check if there already exists a report of the issue you are encountering. + Sorting through duplicates of the same issue sometimes causes more work than + fixing it. Take the time to filter through possible duplicates and be really + sure that your problem definitely is a new one. Try more than one search query + (e.g. do not only search for "webcam" if you happen to run into an issue + with your webcam, also search for "timelapse" etc). + +### What should I include in a bug report? + +Always use the following template (you can remove what's within `[...]`, that's +only provided here as some additional information for you): + + #### What were you doing? + + [Please be as specific as possible here. The maintainers will need to reproduce + your issue in order to fix it and that is not possible if they don't know + what you did to get it to happen in the first place. If you encountered + a problem with specific files of any sorts, make sure to also include a link to a file + with which to reproduce the problem.] + + #### What did you expect to happen? + + #### What happened instead? + + #### Branch & Commit or Version of OctoPrint + + [Can be found in the lower left corner of the web interface.] + + #### Printer model & used firmware incl. version + + [If applicable, always include if unsure.] + + #### Browser and Version of Browser, Operating System running Browser + + [If applicable, always include if unsure.] + + #### Link to octoprint.log + + [On gist.github.com or pastebin.com. Always include and never truncate.] + + #### Link to contents of terminal tab or serial.log + + [On gist.github.com or pastebin.com. If applicable, always include if unsure or + reporting communication issues. Never truncate.] + + #### Link to contents of Javascript console in the browser + + [On gist.github.com or pastebin.com or alternatively a screenshot. If applicable - + always include if unsure or reporting UI issues.] + + #### Screenshot(s) showing the problem: + + [If applicable. Always include if unsure or reporting UI issues.] + + I have read the FAQ. + +### Where can I find which version and branch I'm on? + +You can find out all of them by taking a look into the lower left corner of the +OctoPrint UI: + +![Current version and git branch info in OctoPrint's UI](http://i.imgur.com/HyHMlY2.png) + +If you don't have access to the UI you can find out that information via the +command line as well. Either `octoprint --version` or `python setup.py version` +in OctoPrint's folder will tell you the version of OctoPrint you are running +(note: if it doesn't then you are running a version older than 1.1.0, +*upgrade now*). A `git branch` in your OctoPrint installation folder will mark +the branch you are on with a little *. `git rev-parse HEAD` will tell you the +current commit. + +### Where can I find those log files you keep talking about? + +OctoPrint by default provides two log outputs, a third one can be enabled if +more information is needed. + +One is contained in the **"Terminal" tab** within OctoPrint's UI and is a log +of the last 300 lines of communication with the printer. Please copy-paste +this somewhere (disable auto scroll to make copying the contents easier) - +e.g. http://pastebin.com or http://gist.github.com - and include a link in +your bug report. + +There is also **OctoPrint's application log file** or in short `octoprint.log`, +which is by default located at `~/.octoprint/logs/octoprint.log` on Linux, +`%APPDATA%\OctoPrint\logs\octoprint.log` on Windows and +`~/Library/Application Support/OctoPrint/logs/octoprint.log` on MacOS. Please +copy-paste this to pastebin or gist as well and include a link in your bug +report. + +It might happen that you are asked to provide a more **thorough log of the +communication with the printer** if you haven't already done so, the `serial.log`. +This is not written by default due to performance reasons, but you can enable +it in the settings dialog. After enabling that log, please reproduce the problem +again (connect to the printer, do whatever triggers it), then copy-paste +`~/.octoprint/logs/serial.log` (Windows: `%APPDATA%\OctoPrint\logs\serial.log`, +MacOS: `~/Library/Application Support/OctoPrint/logs/serial.log`) to pastebin +or gist and include the link in the bug report. + +You might also be asked to provide a log with an increased log level. You can +find information on how to do just that in the +[docs](http://docs.octoprint.org/en/master/configuration/logging_yaml.html). + +### Where can I find my browser's error console? + +See [How to open the Javascript Console in different browsers](https://webmasters.stackexchange.com/questions/8525/how-to-open-the-javascript-console-in-different-browsers) + +## Pull requests + +1. If you want to add a new feature to OctoPrint, **please always first + consider if it wouldn't be better suited for a plugin.** As a general rule + of thumb, any feature that is only of interest to a small sub group should + be moved into a plugin. If the current plugin system doesn't allow you to + implement your feature as a plugin, create a "Brainstorming" ticket to get + the discussion going on how best to solve *this* in OctoPrint's plugin + system - maybe that's the actual PR you have been waiting for to contribute :) +2. If you plan to make **any large changes to the code or appearance, please + open a "Brainstorming" ticket first** so that we can determine if it's a + good time for your specific pull request. It might be that we're currently + in the process of making heavy changes to the code locations you'd target + as well, or your approach doesn't fit the general "project vision", and + that would just cause unnecessary work and frustration for everyone or + possibly get the PR rejected. +3. Create your pull request **from a custom branch** on your end (e.g. + `dev/myNewFeature`)[1] **against the `devel` branch**. Create **one pull request + per feature/bug fix**. If your PR contains an important bug fix, we will + make sure to backport it to the `maintenance` branch to also include it in + the next release. +4. Make sure you **follow the current coding style**. This means: + * Tabs instead of spaces in the Python files[2] + * Spaces instead of tabs in the Javascript sources + * English language (code, variables, comments, ...) + * Comments where necessary: Tell why the code does something like it does + it, structure your code + * Following the general architecture + If your PR needs to make changes to the Stylesheets, change the ``.less`` files + from which the CSS is compiled. +5. **Test your changes thoroughly**. That also means testing with usage + scenarios you don't normally use, e.g. if you only use access control, test + without and vice versa. If you only test with your printer, test with the + virtual printer and vice versa. State in your pull request how your tested + your changes. Ideally **add unit tests** - OctoPrint severly lacks in that + department, but we are trying to change that, so any new code already covered + with a test suite helps a lot! +6. In your pull request's description, **state what your pull request is doing**, + as in, what feature does it implement, what bug does it fix. The more + thoroughly you explain your intent behind the PR here, the higher the + chances it will get merged fast. +7. Important: Don't forget to **add yourself to the [AUTHORS](./AUTHORS.md) + file** :) + +## History + + * 2015-01-23: More guidelines for creating pull requests, support/questions + redirected to Mailinglist/G+ community * 2015-01-27: Added another explicit link to the FAQ * 2015-07-07: Added step to add yourself to AUTHORS when creating a PR :) + * 2015-12-01: Heavily reworked to include examples, better structure and + all information in one document. + +## Footnotes + * [1] - If you are wondering why, the problem is that anything that you add + to your PR's branch will also become part of your PR, so if you create a + PR from your version of `devel` chances are high you'll add changes to the + PR that do not belong to the PR. + * [2] - Yes, we know that this goes against PEP-8. OctoPrint started out as + a fork of Cura and hence stuck to the coding style found therein. Changing + it now would make the history and especially `git blame` completely + unusable, so for now we'll have to deal with it (this decision might be + revisited in the future). diff --git a/docs/api/job.rst b/docs/api/job.rst index 3bcc1fde2..eeb3b0718 100644 --- a/docs/api/job.rst +++ b/docs/api/job.rst @@ -37,7 +37,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -54,7 +54,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -71,7 +71,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -88,7 +88,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... diff --git a/docs/api/printer.rst b/docs/api/printer.rst index 480722494..1184965f8 100644 --- a/docs/api/printer.rst +++ b/docs/api/printer.rst @@ -25,7 +25,8 @@ Tool See :ref:`sec-api-printer-toolcommand`. Bed Bed commands allow setting the temperature and temperature offset for the printer's heated bed. Querying the - corresponding resource returns temperature information including an optional history. + corresponding resource returns temperature information including an optional history. Note that Bed commands + are only available if the currently selected printer profile has a heated bed. See :ref:`sec-api-printer-bedcommand`. SD card SD commands allow initialization, refresh and release of the printer's SD card (if available). Querying the @@ -564,6 +565,9 @@ Issue a bed command Upon success, a status code of :http:statuscode:`204` and an empty body is returned. + If no heated bed is configured for the currently selected printer profile, the resource will return + an :http:statuscode:`409`. + **Example Target Temperature Request** Set the target temperature for the printer's heated bed to 75°C. @@ -610,7 +614,8 @@ Issue a bed command :statuscode 204: No error :statuscode 400: If ``target`` or ``offset`` is not a valid number or outside of the supported range, or if the request is otherwise invalid. - :statuscode 409: If the printer is not operational. + :statuscode 409: If the printer is not operational or the selected printer profile + does not have a heated bed. .. _sec-api-printer-bedstate: @@ -627,6 +632,9 @@ Retrieve the current bed state Returns a :http:statuscode:`200` with a Temperature Response in the body upon success. + If no heated bed is configured for the currently selected printer profile, the resource will return + an :http:statuscode:`409`. + .. note:: If you want both tool and bed temperature information at the same time, take a look at :ref:`Retrieve the current printer state `. @@ -675,7 +683,8 @@ Retrieve the current bed state :query limit: If set to an integer (``n``), only the last ``n`` data points from the printer's temperature history will be returned. Will be ignored if ``history`` is not enabled. :statuscode 200: No error - :statuscode 409: If the printer is not operational. + :statuscode 409: If the printer is not operational or the selected printer profile + does not have a heated bed. .. _sec-api-printer-sdcommand: @@ -856,7 +865,7 @@ Send an arbitrary command to the printer .. sourcecode:: http HTTP/1.1 204 No Content - + :json string command: Single command to send to the printer, mutually exclusive with ``commands``. :json string commands: List of commands to send to the printer, mutually exclusive with ``command``. :statuscode 204: No error @@ -913,7 +922,8 @@ Temperature State * - ``bed`` - 0..1 - :ref:`Temperature Data ` - - Current temperature stats for the printer's heated bed. Not included if querying only tool state. + - Current temperature stats for the printer's heated bed. Not included if querying only tool state or if + the currently selected printer profile does not have a heated bed. * - ``history`` - 0..1 - List of :ref:`Historic Temperature Datapoint ` diff --git a/docs/features/gcode_scripts.rst b/docs/features/gcode_scripts.rst index 011e15c36..d1b5fde80 100644 --- a/docs/features/gcode_scripts.rst +++ b/docs/features/gcode_scripts.rst @@ -86,7 +86,7 @@ Out of the box, OctoPrint defaults to the following script setup for ``afterPrin ;disable all heaters {% snippet 'disable_hotends' %} - M140 S0 + [% snippet 'disable_bed' %} ;disable fan M106 S0 @@ -100,8 +100,19 @@ The ``disable_hotends`` snippet is defined as follows: M104 T{{ tool }} S0 {% endfor %} -As you can see, the ``disable_hotends`` snippet utilizes the ``printer_profile`` context variable in order to -iterate through all available extruders and set their temperature to 0. +The ``disable_bed`` snippet is defined as follows: + +.. code-block:: jinja + :caption: Default ``disable_bed`` snippet + + {% if printer_profile.heatedBed %} + M140 S0 + {% endif %} + +As you can see, the ``disable_hotends`` and ``disable_bed`` snippets utilize the +``printer_profile`` context variable in order to iterate through all available +extruders and set their temperature to 0, and to also set the bed temperature +to 0 if a heated bed is configured. .. seealso:: diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..cbda2efbf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[metadata] +description-file = README.md + +[versioneer] +VCS = git +style = pep440-post +versionfile_source = src/octoprint/_version.py +versionfile_build = octoprint/_version.py +tag_prefix = +parentdir_prefix = +lookupfile = .versioneer-lookup diff --git a/setup.py b/setup.py index 80ed83c4b..86b7837f9 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,10 @@ # Documentation dependencies "sphinx>=1.3", "sphinxcontrib-httpdomain", - "sphinx_rtd_theme" + "sphinx_rtd_theme", + + # PyPi upload related + "pypandoc" ], # Dependencies for developing OctoPrint plugins @@ -56,17 +59,12 @@ ] ) +# Additional requirements for setup +SETUP_REQUIRES = [] + # Dependency links for any of the aforementioned dependencies DEPENDENCY_LINKS = [] -# Versioneer configuration -versioneer.VCS = 'git' -versioneer.versionfile_source = 'src/octoprint/_version.py' -versioneer.versionfile_build = 'octoprint/_version.py' -versioneer.tag_prefix = '' -versioneer.parentdir_prefix = '' -versioneer.lookupfile = '.versioneer-lookup' - #----------------------------------------------------------------------------------------------------------------------- # Anything below here is just command setup and general setup configuration @@ -90,8 +88,22 @@ def params(): version = versioneer.get_version() cmdclass = get_cmdclass() - description = "A responsive web interface for 3D printers" + description = "A snappy web interface for 3D printers" long_description = open("README.md").read() + + install_requires = INSTALL_REQUIRES + extras_require = EXTRA_REQUIRES + dependency_links = DEPENDENCY_LINKS + setup_requires = SETUP_REQUIRES + + try: + import pypandoc + setup_requires += ["setuptools-markdown"] + long_description_markdown_filename = "README.md" + del pypandoc + except: + pass + classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", @@ -126,9 +138,6 @@ def params(): include_package_data = True zip_safe = False - install_requires = INSTALL_REQUIRES - extras_require = EXTRA_REQUIRES - dependency_links = DEPENDENCY_LINKS if os.environ.get('READTHEDOCS', None) == 'True': # we can't tell read the docs to please perform a pip install -e .[develop], so we help diff --git a/src/octoprint/_version.py b/src/octoprint/_version.py index 35090f5b8..60161f3c8 100644 --- a/src/octoprint/_version.py +++ b/src/octoprint/_version.py @@ -1,28 +1,81 @@ # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (build by setup.py sdist) and build +# feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. Generated by -# versioneer-0.10 (https://github.com/warner/python-versioneer) - -# these strings will be replaced by git during git-archive -git_refnames = "$Format:%d$" -git_full = "$Format:%H$" +# versioneer-0.15+dev (https://github.com/warner/python-versioneer) +"""Git implementation of _version.py.""" +import errno +import os +import re import subprocess import sys -import errno + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "" + cfg.parentdir_prefix = "" + cfg.versionfile_source = "src/octoprint/_version.py" + cfg.lookupfile = None + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: + dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr @@ -33,7 +86,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): if e.errno == errno.ENOENT: continue if verbose: - print("unable to run %s" % args[0]) + print("unable to run %s" % dispcmd) print(e) return None else: @@ -41,59 +94,67 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): print("unable to find command, tried %s" % (commands,)) return None stdout = p.communicate()[0].strip() - if sys.version >= '3': + if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: - print("unable to run %s (error)" % args[0]) + print("unable to run %s (error)" % dispcmd) return None return stdout -import sys -import re -import os.path +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. -def get_gits(root, verbose=False): - if not os.path.exists(os.path.join(root, ".git")): + Source tarballs conventionally unpack into a directory that includes + both the project name and a version string. + """ + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): if verbose: - print("no .git in %s" % root) - return None - - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - return GITS + print("guessing rootdir is '%s', but '%s' doesn't start with " + "prefix '%s'" % (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} -def get_expanded_variables(versionfile_abs): +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these - # variables. When used from setup.py, we don't want to import - # _version.py, so we do it with a regexp instead. This function is not - # used from _version.py. - variables = {} + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} try: - f = open(versionfile_abs,"r") + f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["refnames"] = mo.group(1) + keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: - variables["full"] = mo.group(1) + keywords["full"] = mo.group(1) f.close() except EnvironmentError: pass - return variables + return keywords -def versions_from_expanded_variables(variables, tag_prefix, verbose=False): - refnames = variables["refnames"].strip() + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: - print("variables are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. @@ -110,6 +171,15 @@ def versions_from_expanded_variables(variables, tag_prefix, verbose=False): tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs-tags)) + + branches = [r for r in refs if not r.startswith(TAG) + and r != "HEAD" and not r.startswith("refs/")] + if verbose: + print("likely branches: %s" % ",".join(sorted(branches))) + branch = None + if branches: + branch = branches[0] + if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): @@ -118,161 +188,473 @@ def versions_from_expanded_variables(variables, tag_prefix, verbose=False): r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) - return { "version": r, - "full": variables["full"].strip() } - # no suitable tags, so we use the full revision id + + result = {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None} + if branch is not None: + result["branch"] = branch + return result + # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: - print("no suitable tags, using full revision id") - return { "version": variables["full"].strip(), - "full": variables["full"].strip() } + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + raise NotThisMethod("no .git directory") + + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # figure out our branch + abbrev_ref_out = run_command(GITS, + ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + if abbrev_ref_out is not None: + pieces["branch"] = abbrev_ref_out.strip() + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +@register_vcs_handler("git", "parse_lookup_file") +def git_parse_lookup_file(path): + """Parse a versioneer lookup file. + + This file allows definition of branch specific data like virtual tags or + custom styles to use for version rendering. + """ + if not os.path.exists(path): + return [] + + import re + lookup = [] + with open(path, "r") as f: + for line in f: + if '#' in line: + line = line[:line.rindex("#")] + line = line.strip() + if not line: + continue + + try: + split_line = map(lambda x: x.strip(), line.split()) + if not len(split_line): + continue + + matcher = re.compile(split_line[0]) + + if len(split_line) == 1: + entry = [matcher, None, None, None] + elif len(split_line) == 2: + render = split_line[1] + entry = [matcher, render, None, None] + elif len(split_line) == 3: + tag, ref_commit = split_line[1:] + entry = [matcher, None, tag, ref_commit] + elif len(split_line) == 4: + tag, ref_commit, render = split_line[1:] + entry = [matcher, render, tag, ref_commit] + else: + continue + + lookup.append(entry) + except: + break + return lookup -def versions_from_lookup(lookup, root, verbose=False): - GITS = get_gits(root, verbose=verbose) - if GITS is None: - return {} + +@register_vcs_handler("git", "pieces_from_lookup") +def git_pieces_from_lookup(lookup, root, verbose, run_command=run_command): + """Extract version information based on provided lookup data.""" + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] stdout = run_command(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) if stdout is None: - return {} + raise NotThisMethod("git rev-parse --abbrev-ref HEAD failed") current_branch = stdout.strip() - for matcher, tag, ref_commit in lookup: + for matcher, render, tag, ref_commit in lookup: if matcher.match(current_branch): if tag is None or ref_commit is None: - return {} + raise NotThisMethod("tag or ref_commit is unset for " + "this branch") - stdout = run_command(GITS, ["rev-list", "%s..HEAD" % ref_commit, "--count"], cwd=root) + stdout = run_command(GITS, + ["rev-list", "%s..HEAD" % ref_commit, + "--count"], + cwd=root) if stdout is None: - return {} - num_commits = stdout.strip() - - stdout =run_command(GITS, ["rev-parse", "--short", "HEAD"], cwd=root) + raise NotThisMethod("git rev-list %s..HEAD " + "--count failed" % ref_commit) + try: + num_commits = int(stdout.strip()) + except ValueError: + raise NotThisMethod("git rev-list %s..HEAD --count didn't " + "return a valid number" % ref_commit) + + stdout = run_command(GITS, + ["rev-parse", "--short", "HEAD"], + cwd=root) if stdout is None: - return {} + raise NotThisMethod("git describe rev-parse " + "--short HEAD failed") short_hash = stdout.strip() - stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], cwd=root) + stdout = run_command(GITS, + ["describe", "--tags", + "--dirty", "--always"], + cwd=root) if stdout is None: - return {} + raise NotThisMethod("git describe --tags --dirty " + "--always failed") dirty = stdout.strip().endswith("-dirty") stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if stdout is None: - return {} + raise NotThisMethod("git rev-parse HEAD failed") full = stdout.strip() - version = "%s-%s-g%s" % (tag, num_commits, short_hash) - if dirty: - version += "-dirty" - full += "-dirty" - return {"version": version, "full": full, "branch": current_branch} - - return {} + return { + "long": full, + "short": short_hash, + "dirty": dirty, + "branch": current_branch, + "closest-tag": tag, + "distance": num_commits, + "error": None, + "render": render + } + + raise NotThisMethod("no matching lookup definition found") + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_dev(pieces): + """TAG[.devDISTANCE]+gHEX[.dirty] . + + Exceptions: + 1: no tags. 0.devDISTANCE+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".dev%d" % pieces["distance"] + rendered += plus_or_dot(pieces) + else: + # exception #1 + rendered = "0.dev%d" % pieces["distance"] + rendered += "+" + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered -def versions_from_vcs(tag_prefix, root, verbose=False): - # this runs 'git' from the root of the source tree. This only gets called - # if the git-archive 'subst' variables were *not* expanded, and - # _version.py hasn't already been rewritten with a short version string, - # meaning we're inside a checked out source tree. - GITS = get_gits(root, verbose=verbose) - if GITS is None: - return {} +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. - stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], - cwd=root) - if stdout is None: - return {} - if not stdout.startswith(tag_prefix): - if verbose: - print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) - return {} - tag = stdout[len(tag_prefix):] - stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if stdout is None: - return {} - full = stdout.strip() - if tag.endswith("-dirty"): - full += "-dirty" + Like 'git describe --tags --dirty --always'. - stdout = run_command(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) - if stdout is None: - branch = None + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: - branch = stdout.strip() - return {"version": tag, "full": full, "branch": branch} + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered -def versions_from_parentdir(parentdir_prefix, root, verbose=False): - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % - (root, dirname, parentdir_prefix)) - return None - return {"version": dirname[len(parentdir_prefix):], "full": "", "branch": ""} +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. -tag_prefix = "" -parentdir_prefix = "" -versionfile_source = "src/octoprint/_version.py" -lookupfile = ".versioneer-lookup" + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. -def parse_lookup_file(root, lookup_path=None): - if not lookup_path: - lookup_path = lookupfile - if not lookup_path: - return [] + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if "render" in pieces and pieces["render"] is not None: + style = pieces["render"] + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "pep440-dev": + rendered = render_pep440_dev(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) - path = os.path.join(root, lookup_path) - if not os.path.exists(path): - return [] + result = {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + if "branch" in pieces and pieces["branch"] is not None: + result["branch"] = pieces["branch"] + return result - import re - lookup = [] - with open(os.path.join(root, lookup_path), "r") as f: - for line in f: - if '#' in line: - line = line[:line.rindex('#')] - line = line.strip() - try: - split_line = line.split() - if len(split_line) == 3: - pattern, tag, ref_commit = split_line - lookup.append([re.compile(pattern), tag, ref_commit]) - elif len(split_line) >= 1: - lookup.append([re.compile(split_line[0]), None, None]) - except: - break - return lookup -def get_versions(default={"version": "unknown", "full": "", "branch": "unknown"}, lookup_path=None, verbose=False): +def get_versions(): + """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded variables. + # case we can only use expanded keywords. - variables = { "refnames": git_refnames, "full": git_full } - ver = versions_from_expanded_variables(variables, tag_prefix, verbose) - if ver: - return ver + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass try: - root = os.path.abspath(__file__) + root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in range(len(versionfile_source.split("/"))): + for i in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: - return default + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} - lookup = parse_lookup_file(root, lookup_path=lookup_path) - return (versions_from_lookup(lookup, root, verbose) - or versions_from_vcs(tag_prefix, root, verbose) - or versions_from_parentdir(parentdir_prefix, root, verbose) - or default) + lookupfile = cfg.lookupfile if cfg.lookupfile is not None else ".versioneer-lookup" + lookuppath = os.path.join(root, lookupfile) + if os.path.exists(lookuppath): + try: + lookup_data = git_parse_lookup_file(lookuppath) + pieces = git_pieces_from_lookup(lookup_data, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 9f238cf93..6a2378dd6 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -13,6 +13,8 @@ import octoprint.filemanager +from octoprint.util import is_hidden_path + class StorageInterface(object): """ Interface of storage adapters for OctoPrint. @@ -357,7 +359,7 @@ def _analysis_backlog_generator(self, path=None): if not metadata: metadata = dict() for entry in os.listdir(path): - if entry.startswith(".") or not octoprint.filemanager.valid_file_type(entry): + if is_hidden_path(entry) or not octoprint.filemanager.valid_file_type(entry): continue absolute_path = os.path.join(path, entry) @@ -584,24 +586,6 @@ def sanitize(self, path): Note that for a ``path`` without a trailing slash the last part will be considered a file name and hence be returned at second position. If you only need to convert a folder path, be sure to include a trailing slash for a string ``path`` or an empty last element for a list ``path``. - - Examples:: - - >>> storage = LocalFileStorage("/some/base/folder") - >>> storage.sanitize("some/folder/and/some file.gco") - ("/some/base/folder/some/folder/and", "some_file.gco") - >>> storage.sanitize(("some", "folder", "and", "some file.gco")) - ("/some/base/folder/some/folder/and", "some_file.gco") - >>> storage.sanitize("some file.gco") - ("/some/base/folder", "some_file.gco") - >>> storage.sanitize(("some file.gco",)) - ("/some/base/folder", "some_file.gco") - >>> storage.sanitize("") - ("/some/base/folder", "") - >>> storage.sanitize("some/folder/with/trailing/slash/") - ("/some/base/folder/some/folder/with/trailing/slash", "") - >>> storage.sanitize("some", "folder", "") - ("/some/base/folder/some/folder", "") """ name = None if isinstance(path, (str, unicode, basestring)): @@ -628,24 +612,6 @@ def sanitize_name(self, name): Raises a :class:`ValueError` for a ``name`` containing ``/`` or ``\``. Otherwise strips any characters from the given ``name`` that are not any of the ASCII characters, digits, ``-``, ``_``, ``.``, ``(``, ``)`` or space and replaces and spaces with ``_``. - - Examples:: - - >>> storage = LocalFileStorage("/some/base/folder") - >>> storage.sanitize_name("some_file.gco") - "some_file.gco" - >>> storage.sanitize_name("some_file with (parentheses) and ümläuts and digits 123.gco") - "some_file_with_(parentheses)_and_mluts_and_digits_123.gco" - >>> storage.sanitize_name("pengüino pequeño.stl") - "pengino_pequeo.stl" - >>> storage.sanitize_name("some/folder/still/left.gco") - Traceback (most recent call last): - File "", line 1, in - ValueError: name must not contain / or \ - >>> storage.sanitize_name("also\\no\\backslashes.gco") - Traceback (most recent call last): - File "", line 1, in - ValueError: name must not contain / or \ """ if name is None: return None @@ -664,22 +630,6 @@ def sanitize_path(self, path): Ensures that the on disk representation of ``path`` is located under the configured basefolder. Resolves all relative path elements (e.g. ``..``) and sanitizes folder names using :func:`sanitize_name`. Final path is the absolute path including leading ``basefolder`` path. - - Examples:: - - >>> storage = LocalFileStorage("/some/base/folder") - >>> storage.sanitize_path("folder/with/subfolder") - "/some/base/folder/folder/with/subfolder" - >>> storage.sanitize_path("folder/with/subfolder/../other/folder") - "/some/base/folder/folder/with/other/folder" - >>> storage.sanitize_path("/folder/with/leading/slash") - "/some/base/folder/folder/with/leading/slash" - >>> storage.sanitize_path(".folder/with/leading/dot") - "/some/base/folder/folder/with/leading/dot - >>> storage.sanitize_path("../../folder/out/of/the/basefolder") - Traceback (most recent call last): - File "", line 1, in - ValueError: path not contained in base folder: /some/folder/out/of/the/basefolder """ if path[0] == "/" or path[0] == ".": path = path[1:] @@ -939,7 +889,7 @@ def _list_folder(self, path, filter=None, recursive=True): result = dict() for entry in os.listdir(path): - if entry.startswith("."): + if is_hidden_path(entry): # no hidden files and folders continue diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 21976d08b..0cc587150 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -293,19 +293,16 @@ def __init__(self, settings, plugin_key, defaults=None, get_preprocessors=None, self.set_preprocessors = dict(plugins=dict()) self.set_preprocessors["plugins"][plugin_key] = set_preprocessors - def prefix_path(path): - return ['plugins', self.plugin_key] + path - def prefix_path_in_args(args, index=0): result = [] if index == 0: - result.append(prefix_path(args[0])) + result.append(self._prefix_path(args[0])) result.extend(args[1:]) else: args_before = args[:index - 1] args_after = args[index + 1:] result.extend(args_before) - result.append(prefix_path(args[index])) + result.append(self._prefix_path(args[index])) result.extend(args_after) return result @@ -324,6 +321,7 @@ def add_setter_kwargs(kwargs): return kwargs self.access_methods = dict( + has =("has", prefix_path_in_args, add_getter_kwargs), get =("get", prefix_path_in_args, add_getter_kwargs), get_int =("getInt", prefix_path_in_args, add_getter_kwargs), get_float =("getFloat", prefix_path_in_args, add_getter_kwargs), @@ -331,7 +329,8 @@ def add_setter_kwargs(kwargs): set =("set", prefix_path_in_args, add_setter_kwargs), set_int =("setInt", prefix_path_in_args, add_setter_kwargs), set_float =("setFloat", prefix_path_in_args, add_setter_kwargs), - set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs) + set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs), + remove =("remove", prefix_path_in_args) ) self.deprecated_access_methods = dict( getInt ="get_int", @@ -342,6 +341,17 @@ def add_setter_kwargs(kwargs): setBoolean="set_boolean" ) + def _prefix_path(self, path=None): + if path is None: + path = list() + return ['plugins', self.plugin_key] + path + + def global_has(self, path, **kwargs): + return self.settings.has(path, **kwargs) + + def global_remove(self, path, **kwargs): + return self.settings.remove(path, **kwargs) + def global_get(self, path, **kwargs): """ Getter for retrieving settings not managed by the plugin itself from the core settings structure. Use this @@ -435,6 +445,24 @@ def get_plugin_data_folder(self): os.makedirs(path) return path + def get_all_data(self, **kwargs): + merged = kwargs.get("merged", True) + asdict = kwargs.get("asdict", True) + defaults = kwargs.get("defaults", self.defaults) + preprocessors = kwargs.get("preprocessors", self.get_preprocessors) + + kwargs.update(dict( + merged=merged, + asdict=asdict, + defaults=defaults, + preprocessors=preprocessors + )) + + return self.settings.get(self._prefix_path(), **kwargs) + + def clean_all_data(self): + self.settings.remove(self._prefix_path()) + def __getattr__(self, item): all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys() if item in all_access_methods: diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index bf702cb68..d1987ea6e 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -651,6 +651,24 @@ def decorator(f): return f return decorator + @staticmethod + def errorhandler(code_or_exception): + """ + A decorator to mark errorhandlings methods in your BlueprintPlugin subclass. Works just the same as Flask's + own ``errorhandler`` decorator available on blueprints. + + See `the documentation for flask.Blueprint.errorhandler `_ + and `the documentation for flask.Flask.errorhandler `_ for more + information. + """ + from collections import defaultdict + def decorator(f): + if not hasattr(f, "_blueprint_error_handler") or f._blueprint_error_handler is None: + f._blueprint_error_handler = defaultdict(list) + f._blueprint_error_handler[f.__name__].append(code_or_exception) + return f + return decorator + def get_blueprint(self): """ Creates and returns the blueprint for your plugin. Override this if you want to define and handle your blueprint yourself. @@ -660,15 +678,34 @@ def get_blueprint(self): :return: the blueprint ready to be registered with Flask """ + if hasattr(self, "_blueprint"): + # if we already constructed the blueprint and hence have it cached, + # return that instance - we don't want to instance it multiple times + return self._blueprint + import flask kwargs = self.get_blueprint_kwargs() blueprint = flask.Blueprint("plugin." + self._identifier, self._identifier, **kwargs) + + # we now iterate over all members of ourselves and look if we find an attribute + # that has data originating from one of our decorators - we ignore anything + # starting with a _ to only handle public stuff for member in [member for member in dir(self) if not member.startswith("_")]: f = getattr(self, member) + if hasattr(f, "_blueprint_rules") and member in f._blueprint_rules: + # this attribute was annotated with our @route decorator for blueprint_rule in f._blueprint_rules[member]: rule, options = blueprint_rule blueprint.add_url_rule(rule, options.pop("endpoint", f.__name__), view_func=f, **options) + + if hasattr(f, "_blueprint_error_handler") and member in f._blueprint_error_handler: + # this attribute was annotated with our @error_handler decorator + for code_or_exception in f._blueprint_error_handler[member]: + blueprint.errorhandler(code_or_exception)(f) + + # cache and return the blueprint object + self._blueprint = blueprint return blueprint def get_blueprint_kwargs(self): @@ -764,6 +801,9 @@ def on_after_startup(self): the plugin core system upon initialization of the implementation. """ + config_version_key = "_config_version" + """Key of the field in the settings that holds the configuration format version.""" + def on_settings_load(self): """ Loads the settings for the plugin, called by the Settings API view in order to retrieve all settings from @@ -781,9 +821,9 @@ def on_settings_load(self): :return: the current settings of the plugin, as a dictionary """ - data = self._settings.get([], asdict=True, merged=True) - if "_config_version" in data: - del data["_config_version"] + data = self._settings.get_all_data() + if self.config_version_key in data: + del data[self.config_version_key] return data def on_settings_save(self, data): @@ -795,7 +835,8 @@ def on_settings_save(self, data): .. note:: The default implementation will persist your plugin's settings as is, so just in the structure and in the - types that were received by the Settings API view. + types that were received by the Settings API view. Values identical to the default settings values + will *not* be persisted. If you need more granular control here, e.g. over the used data types, you'll need to override this method and iterate yourself over all your settings, retrieving them (if set) from the supplied received ``data`` @@ -803,15 +844,38 @@ def on_settings_save(self, data): Arguments: data (dict): The settings dictionary to be saved for the plugin + + Returns: + dict: The settings that differed from the defaults and were actually saved. """ import octoprint.util - if "_config_version" in data: - del data["_config_version"] + # get the current data + current = self._settings.get_all_data() + if current is None: + current = dict() + + # merge our new data on top of it + new_current = octoprint.util.dict_merge(current, data) + if self.config_version_key in new_current: + del new_current[self.config_version_key] + + # determine diff dict that contains minimal set of changes against the + # default settings - we only want to persist that, not everything + diff = octoprint.util.dict_minimal_mergediff(self.get_settings_defaults(), new_current) + + version = self.get_settings_version() + + to_persist = dict(diff) + if version: + to_persist[self.config_version_key] = version + + if to_persist: + self._settings.set([], to_persist) + else: + self._settings.clean_all_data() - current = self._settings.get([], asdict=True, merged=True) - merged = octoprint.util.dict_merge(current, data) - self._settings.set([], merged) + return diff def get_settings_defaults(self): """ @@ -894,6 +958,49 @@ def on_settings_migrate(self, target, current): """ pass + def on_settings_cleanup(self): + """ + Called after migration and initialization but before call to :func:`on_settings_initialized`. + + Plugins may overwrite this method to perform additional clean up tasks. + + The default implementation just minimizes the data persisted on disk to only contain + the differences to the defaults (in case the current data was persisted with an older + version of OctoPrint that still duplicated default data). + """ + import octoprint.util + from octoprint.settings import NoSuchSettingsPath + + try: + # let's fetch the current persisted config (so only the data on disk, + # without the defaults) + config = self._settings.get_all_data(merged=False, incl_defaults=False, error_on_path=True) + except NoSuchSettingsPath: + # no config persisted, nothing to do => get out of here + return + + if config is None: + # config is set to None, that doesn't make sense, kill it and leave + self._settings.clean_all_data() + return + + if self.config_version_key in config and config[self.config_version_key] is None: + # delete None entries for config version - it's the default, no need + del config[self.config_version_key] + + # calculate a minimal diff between the settings and the current config - + # anything already in the settings will be removed from the persisted + # config, no need to duplicate it + defaults = self.get_settings_defaults() + diff = octoprint.util.dict_minimal_mergediff(defaults, config) + + if not diff: + # no diff to defaults, no need to have anything persisted + self._settings.clean_all_data() + else: + # diff => persist only that + self._settings.set([], diff) + def on_settings_initialized(self): """ Called after the settings have been initialized and - if necessary - also been migrated through a call to diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index fd326a109..095a274b5 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -37,11 +37,10 @@ def __init__(self): ##~~ TemplatePlugin API - def get_template_configs(self): - from flask.ext.babel import gettext - return [ - dict(type="settings", name=gettext("CuraEngine")) - ] + def get_template_vars(self): + return dict( + homepage=__plugin_url__ + ) ##~~ StartupPlugin API @@ -413,9 +412,9 @@ def _sanitize_name(name): sanitized_name = sanitized_name.replace(" ", "_") return sanitized_name.lower() -__plugin_name__ = "CuraEngine" +__plugin_name__ = "CuraEngine (<= 15.04)" __plugin_author__ = "Gina Häußge" __plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura" -__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint" +__plugin_description__ = "Adds support for slicing via CuraEngine versions up to and including version 15.04 from within OctoPrint" __plugin_license__ = "AGPLv3" __plugin_implementation__ = CuraPlugin() diff --git a/src/octoprint/plugins/cura/templates/cura_settings.jinja2 b/src/octoprint/plugins/cura/templates/cura_settings.jinja2 index 6779f95c2..92cfe0ae0 100644 --- a/src/octoprint/plugins/cura/templates/cura_settings.jinja2 +++ b/src/octoprint/plugins/cura/templates/cura_settings.jinja2 @@ -1,5 +1,12 @@

{{ _('General') }}

+

{% trans %} + Specify the path to the CuraEngine binary. Note that only + versions up to and including 15.04 are supported. + CuraEngine version 15.06 or newer is not + compatible with this plugin. +{% endtrans %}

+
@@ -10,7 +17,7 @@
@@ -53,6 +60,10 @@ +
+ {% trans %}For more information on configuration and usage please see the Plugin's homepage.{% endtrans %} +
+ + + {% trans %} + You can import your existing profile .ini files from Cura (version up to and + including 15.04) here. Please be aware that neither the .json profile format + from Cura versions starting with 15.06 is supported, nor are the custom Cura profile formats + that third party tools like e.g. Repetier create. + {% endtrans %}