diff options
Diffstat (limited to 'external/plyer')
101 files changed, 6580 insertions, 0 deletions
diff --git a/external/plyer/CHANGELOG.md b/external/plyer/CHANGELOG.md new file mode 100644 index 0000000..20f5bcf --- /dev/null +++ b/external/plyer/CHANGELOG.md @@ -0,0 +1,939 @@ +Changelog +========= + +%%version%% (unreleased) +------------------------ + +- Update __init__.py. [dessant] + +- Update copyright year. [dessant] + +- Fix windows filechooser using `mode` instead of `self.mode` [gabriel + pettier] + +- Merge pull request #144 from thegrymek/plyer-audio. [dessant] + + Plyer audio for android with facade and example + +- Update docs facade with permissions. [andrzej.grymkowski] + +- Improving docs. [andrzej.grymkowski] + +- Corrected "" in docstring. [Andrzej Grymkowski] + +- Flake8. [Andrzej Grymkowski] + +- Update readme. [Andrzej Grymkowski] + +- Removing temporary files. [Andrzej Grymkowski] + +- Moved example class to facades. [Andrzej Grymkowski] + +- Impoved example. [andrzej.grymkowski] + +- Added almost working example. [Andrzej Grymkowski] + +- Initial commit. [Andrzej Grymkowski] + +- Merge pull request #147 from kivy/stylefix. [dessant] + + style fixes + +- Style fixes. [dessant] + +- Merge pull request #146 from kivy/dessant-patch-1. [dessant] + + add pydev files to gitignore + +- Add pydev files to gitignore. [dessant] + +- Merge pull request #145 from thegrymek/plyer-style-guide-update. + [dessant] + + Plyer style guide update + +- Another improvements. [andrzej.grymkowski] + +- Corrected win filechooser. [andrzej.grymkowski] + +- Update for docs. [andrzej.grymkowski] + +- Improved styles in files. [andrzej.grymkowski] + +- Updated kivy pep8 and corrected some files. [andrzej.grymkowski] + +- Merge pull request #143 from kivy/dessant-patch-1. [dessant] + + fix versionchanged tag + +- Fix versionchanged tag. [dessant] + +- Merge pull request #133 from aron-bordin/master. [dessant] + + fix #107 - Use Android_ID instead of IMEI + +- Fix #107 - Use Android_ID instead of IMEI. [Aron Bordin] + +- Merge pull request #140 from thegrymek/plyer-android4.0-email. + [dessant] + + update info about support email for android<4.0 + +- Update info to readme about support email on android<4.0. + [andrzej.grymkowski] + +- Merge pull request #135 from thegrymek/inclement-orientation. + [dessant] + + Inclement orientation + +- Adding tag versionadded to facade. [andrzej.grymkowski] + +- Fix for import facade orientation. [Andrzej Grymkowski] + +- Added to readme supported platforms platform android < 4.0. [Andrzej + Grymkowski] + +- Removing red functions label. [Andrzej Grymkowski] + +- Corrected readme. [Andrzej Grymkowski] + +- Updated orientation. [Andrzej Grymkowski] + +- Merge pull request #139 from thegrymek/plyer-add-package-facades- + setuptools. [dessant] + + added plyer.facade to setuptools package + +- Added plyer.facade to setuptools package. [Andrzej Grymkowski] + +- Merge pull request #138 from thegrymek/plyer-split-facades. [dessant] + + splitted facades + +- Changed order. [Andrzej Grymkowski] + +- Spliting end. [Andrzej Grymkowski] + +- Spliting end. [Andrzej Grymkowski] + +- First part. [Andrzej Grymkowski] + +- Merge pull request #129 from thegrymek/android-vibrator-sdk-9. + [dessant] + + vibrator for android v < 4.0 + +- Added defaults. [Andrzej Grymkowski] + +- Removing files tests. [Andrzej Grymkowski] + +- A. [Andrzej Grymkowski] + +- Merging. [Andrzej Grymkowski] + +- Replace """ in docstrings" [Andrzej Grymkowski] + +- Added exist for sdk < 11. [Andrzej Grymkowski] + +- Added unsupported exception. [Andrzej Grymkowski] + +- Removed unnessesary docstrings. [Andrzej Grymkowski] + +- Conflicts main. [Andrzej Grymkowski] + +- Updated document. [Andrzej Grymkowski] + +- Vibrator for android v < 4.0, pep257 and flake8. [Andrzej Grymkowski] + +- Updated document. [Andrzej Grymkowski] + +- Remove unnessesary docstring. [Andrzej Grymkowski] + +- Merging. [Andrzej Grymkowski] + +- Merge branch 'new_branch_name' [Andrzej Grymkowski] + +- Merge. [Andrzej Grymkowski] + +- Remove unused variables. [thegrymek] + +- PEP8 and typo fixes in MacOS X file chooser. [Robert Jerovsek] + +- Removed unused imports and refactor. [Andrzej Grymkowski] + +- Pep8 - removed unused imports and variables. [Andrzej Grymkowski] + +- Make pep8 compatible. [laltin] + +- Responds to issue 109 https://github.com/kivy/plyer/issues/109. + [albericc] + +- As. [Andrzej Grymkowski] + +- Added first structure. [Andrzej Grymkowski] + +- Merge pull request #1 from kivy/master. [thegrymek] + + sync pull request + +- Merge pull request #134 from thegrymek/patch-1. [dessant] + + remove unused variables + +- Remove unused variables. [thegrymek] + +- Merge pull request #110 from AlbericC/issue_109. [dessant] + + responds to issue 109 https://github.com/kivy/plyer/issues/109 + +- Responds to issue 109 https://github.com/kivy/plyer/issues/109. + [albericc] + +- Merge pull request #121 from laltin/pep8_fix. [Mathieu Virbel] + + Pep8 fix + +- Make pep8 compatible. [laltin] + +- Merge pull request #1 from kivy/master. [Lütfi Altın] + + sync with origin + +- Merge pull request #122 from thegrymek/pep8-removed-unused-imports- + and-variables. [Mathieu Virbel] + + pep8 - removed unused imports and variables + +- Removed unused imports and refactor. [Andrzej Grymkowski] + +- Pep8 - removed unused imports and variables. [Andrzej Grymkowski] + +- Merge pull request #123 from robertjerovsek/master. [Mathieu Virbel] + + PEP8 and typo fixes in MacOS X file chooser. + +- PEP8 and typo fixes in MacOS X file chooser. [Robert Jerovsek] + +- Merge pull request #106 from Davideddu/filechooser. [Mathieu Virbel] + + Add file chooser facade and support for Linux and Windows + +- Fix filter conversion on Mac OS X. [Davide Depau] + +- Add experimental support for Mac OS X. [Davide Depau] + +- Fix inheritance issue on windows filechooser implementation. [Davide + Depau] + +- Add filechooser facade and support for Linux and Windows. [Davide + Depau] + +- Merge pull request #119 from trivedigaurav/use-environ-lang. [Mathieu + Virbel] + + Use environ to change LANG to 'C' while calling shell processes + +- Use environ to change LANG to 'C' while calling shell processes. + [gtrivedi] + +- Merge pull request #120 from pspchucky/master. [Mathieu Virbel] + + add video recoding to Camera facade and camera.py + +- Update camera.py. [pspchucky] + +- Add video capture support to facades.py. [pspchucky] + +- Merge pull request #118 from kived/irblaster-facade. [Akshay Arora] + + add IrBlaster facade and Android implementation + +- Add IrBlaster facade and Android implementation. [Ryan Pessa] + +- Update README.rst. [Mathieu Virbel] + +- Merge pull request #116 from laltin/ios_gps. [Mathieu Virbel] + + iOS GPS support + +- Cleanup & comment. [laltin] + +- First working version. [laltin] + +- Merge pull request #115 from kivy/fixes_114. [Mathieu Virbel] + + use environ to change LANG to 'C' while calling lshw + +- Use environ to change LANG to 'C' while calling lshw. [gabriel + pettier] + + fixes #114 + +- Merge pull request #117 from JimmyStavros/patch-1. [Mathieu Virbel] + + Android gps.py: fixed location provider cycling + +- Android gps.py: fixed location provider cycling. [JimmyStavros] + + Issue was first brought to light here: https://github.com/kivy/plyer/issues/54 + + GPS module was cycling through "gps" three times instead of all location services. + +- Fixes hashCode overflow. Latest kivy/pyjnius master have already + hashCode/equals/toString implementation, and the hashCode is fixed + with overflow. Just dont try to implement it here. Closes #103. Ref + kivy/pyjnius#146. [Mathieu Virbel] + +- Bump to 1.2.4-dev. [Mathieu Virbel] + +1.2.3 (2015-01-27) +------------------ + +- Bump to 1.2.3. [Mathieu Virbel] + +- Setup.py: add changelog into the description + fix rst issue. [Mathieu + Virbel] + +- Bump to 1.2.3-dev. [Mathieu Virbel] + +1.2.2 (2015-01-27) +------------------ + +- Bump to 1.2.2. [Mathieu Virbel] + +- Update the version to the next dev (missing from the last release) + [Mathieu Virbel] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Merge pull request #102 from helenst/sys-platform-linux-check. + [Mathieu Virbel] + + Linux platform check made compatible with python 3.3+ (Fixes #58) + +- Linux platform check made compatible with python 3.3+ (Fixes #58) + [Helen ST] + +- Add initial changelog. [Mathieu Virbel] + +- Plyer: fix androidd notification. Closes #93 (credits to @kashifpk) + [Mathieu Virbel] + +- Android/notification: implement a switch to allow usage of API < 16. + [Mathieu Virbel] + +1.2.1 (2014-08-19) +------------------ + +- Merge pull request #92 from dessant/patch-2. [trivedigaurav] + + fix print statement + +- Fix print statement. [dessant] + +- SMS Manager is supported since Android 1.6. [trivedigaurav] + +- Merge pull request #90 from trivedigaurav/ios_uuid. [trivedigaurav] + + iOS UUID facade + +- IOS UUID facade. [gtrivedi] + +- Merge pull request #86 from trivedigaurav/ios_battery. [trivedigaurav] + + iOS Battery + +- Change get_status to get_state. [gtrivedi] + +- Clean up. [gtrivedi] + +- Add battery.py. [gtrivedi] + +- IOS Battery Facade. [gtrivedi] + +- Update compass.py. [trivedigaurav] + +- Update gyroscope.py. [trivedigaurav] + +- Fix typo. [trivedigaurav] + +- Fix typo. [trivedigaurav] + +- Fix style. [gtrivedi] + +- Merge branch 'trivedigaurav-ios_tts' [gtrivedi] + +- Fixing README confict and merge master. [gtrivedi] + +- Merge pull request #88 from trivedigaurav/ios_email. [trivedigaurav] + + iOS Email Facade + +- Add email.py. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into ios_email. + [gtrivedi] + +- Merge pull request #89 from trivedigaurav/fix_make. [trivedigaurav] + + Removing build_ext from plyer + +- Removing build_ext from plyer. [gtrivedi] + +- Update accelerometer.py. [trivedigaurav] + +- Python 3 compat. [trivedigaurav] + +- Python 3 compat. [trivedigaurav] + +- Python 3 compat. [trivedigaurav] + +- IOS Email Facade. [gtrivedi] + +- Merge pull request #82 from trivedigaurav/sensors_start_none. + [trivedigaurav] + + Fix Android enable and disable. Return (None, None, None) until sensor data is available + +- Fix sensor enable-disable. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into + sensors_start_none. [gtrivedi] + +- Merge pull request #68 from trivedigaurav/linux_accel. [trivedigaurav] + + Linux accelerometer facade + +- Linux accelerometer facade. [gtrivedi] + +- Fix style error. [trivedigaurav] + +- Merge pull request #85 from trivedigaurav/battery_ischarging. + [trivedigaurav] + + Change connected to isCharging + +- Change connected to isCharging. [gtrivedi] + +- Merge pull request #80 from ChrisCole42/patch-2. [trivedigaurav] + + Update compass.py + +- Update compass.py. [ChrisCole42] + + copied the missing definitions across from accelerometer.py + +- Merge pull request #79 from trivedigaurav/where_is. [trivedigaurav] + + Use whereis_exe to check for binaries + +- Remove trailing newline. [gtrivedi] + +- Add whereis_exe import. [gtrivedi] + +- Remove trailing newline. [gtrivedi] + +- Use whereis_exe to check for binaries. [gtrivedi] + +- Fix style errors. [gtrivedi] + +- Copy values atomically. [gtrivedi] + +- Return None untill data is available. [gtrivedi] + +- Merge pull request #77 from ChrisCole42/patch-1. [trivedigaurav] + + Update compass.py + +- Update compass.py. [ChrisCole42] + + fix "global name is not defined" error in AndroidCompass.__init__() + +- Merge pull request #75 from trivedigaurav/maintenance. [trivedigaurav] + + Maintenance merge + +- Remove extra make command. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into + maintenance. [gtrivedi] + + Conflicts: + plyer/facades.py + +- Changed battery Xs to correct columns (ios -> win) [Alexander Taylor] + +- Really did fix battery formatting in readme. [Alexander Taylor] + +- Fixed battery formatting in readme. [Alexander Taylor] + +- Merge pull request #74 from dessant/patch-1. [Akshay Arora] + + facade docstring revision + +- Facade docstring revision. [dessant] + +- Merge pull request #73 from trivedigaurav/battery_info. [Akshay Arora] + + Query Battery info/status + +- Merge pull request #71 from trivedigaurav/master. [trivedigaurav] + + Revert "Activity was imported twice" + +- Revert "Activity was imported twice" [gtrivedi] + + This reverts commit a0600929774c1e90c7dc43043ff87b5ea84213b4. + +- Activity was imported twice. [trivedigaurav] + +- Fix style errors after merging with master. [gtrivedi] + +- Fix styles errors in libs/ [gtrivedi] + +- Don't ignore lib folder. [gtrivedi] + +- Fix style errors. [gtrivedi] + +- Fix docstrings. [gtrivedi] + +- Fix typo and style. [gtrivedi] + +- Copy pep8 style checker from Kivy. [gtrivedi] + +- Add Makefile. [gtrivedi] + +- Add windows implementation. [gtrivedi] + +- Fix android implemenation typos. [gtrivedi] + +- Battery facade. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into + battery_info. [gtrivedi] + + Conflicts: + plyer/__init__.py + +- Merge pull request #70 from trivedigaurav/master. [trivedigaurav] + + Fix tabbing + +- Merge pull request #69 from trivedigaurav/gyroscope_fix. + [trivedigaurav] + + Gyroscope facade proxy declarations + +- Gyroscope facade proxy declarations. [gtrivedi] + +- Merge pull request #67 from trivedigaurav/patch-1. [Akshay Arora] + + Update README.rst + +- Update README.rst. [trivedigaurav] + + We have merged commits for the email facades on desktops. + +- Merge branch 'master' of https://github.com/trivedigaurav/plyer into + battery_info. [gtrivedi] + +- Fix tabbing. [trivedigaurav] + + Not sure how this passed through. + +- Battery facade. [gtrivedi] + +- Typo. [Mathieu Virbel] + +- Ios: gyroscope is also supported now. [Mathieu Virbel] + +1.2.0 (2014-06-24) +------------------ + +- Bump to 1.2.0, and mark new classes to 1.2.0. [Mathieu Virbel] + +- Merge master. [Mathieu Virbel] + +- Remove dependency on Hardware.java. [gtrivedi] + +- IOS and Android implementations. [gtrivedi] + +- Gyroscope facade. [gtrivedi] + +- Add gyroscope in README. [gtrivedi] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Merge pull request #49 from Davideddu/macosx_email. [Mathieu Virbel] + + add Mac OS X email support + +- Add Mac OS X email support. [Davide Depau] + +- Merge pull request #48 from Davideddu/windows_email. [Mathieu Virbel] + + add Windows email support + +- Catch exception if no email client is installed on Windows. [Davide + Depau] + +- Add Windows email support. [Davide Depau] + +- Merge pull request #47 from Davideddu/linux_email. [Mathieu Virbel] + + added Linux email support + +- Revert previous commit, use xdg-open to open email client. [Davide + Depau] + +- Use xdg-email by default, fallback to xdg-open + mailto uri. [Davide + Depau] + +- Added Linux email support. [Davide Depau] + +- Merge master. [Mathieu Virbel] + +- Remove gyroscope files. [gtrivedi] + +- Fix typos introduced while copying. [gtrivedi] + +- Fix typo. [gtrivedi] + +- Fix tabbing. [gtrivedi] + +- IOS Magnetometer. [gtrivedi] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Merge pull request #63 from trivedigaurav/pyjnius_accel. [Mathieu + Virbel] + + Switched to pyjnius + +- Make values assignment atomic. [gtrivedi] + +- Switched to pyjnius. [gtrivedi] + +- Merge master. [Mathieu Virbel] + +- Plyer Unique ID facade. [gtrivedi] + +- Merge master. [Mathieu Virbel] + +- Update README. [trivedigaurav] + +- Merge branch 'android_compass' of + https://github.com/trivedigaurav/plyer into android_compass. + [gtrivedi] + +- Update buildozer.spec. [trivedigaurav] + +- Pyjnius compass. [gtrivedi] + +- Update buildozer.spec. [gtrivedi] + +- Plyer compass facade. [gtrivedi] + +- Merge pull request #52 from mihaineacsu/sms. [Mathieu Virbel] + + Added sms facade, example and android implementation + +- Updated sms components. [mihaineacsu] + +- Fix components of the sms facade. [mihaineacsu] + +- Update README with sms feature. [mihaineacsu] + +- Update plyer init. [mihaineacsu] + +- Add sms example. [mihaineacsu] + +- Add initial android sms implementation. [mihaineacsu] + +- Add sms facade. [mihaineacsu] + +- Merge pull request #55 from trivedigaurav/osx_accel. [Mathieu Virbel] + + Using sudden motion sensor as accelerometer on OSX + +- Check accelerometer in OSX. [gtrivedi] + +- LGPL notice no longer required. [gtrivedi] + +- Fix tabs to spaces. [gtrivedi] + +- Switched to ctypes. [gtrivedi] + +- Remove osx libs in setup.py. [gtrivedi] + +- Added LGPL.txt for unimotion.c. [gtrivedi] + +- Added libs/ folder in macosx. [gtrivedi] + +- OSX Accel facade using unimotion. [gtrivedi] + +- Merge pull request #62 from trivedigaurav/patch-2. [Mathieu Virbel] + + Update README + +- Update README. [trivedigaurav] + +- Merge pull request #56 from trivedigaurav/patch-1. [Akshay Arora] + + Update README + +- Update README. [trivedigaurav] + +- Remove buildozer db. [Mathieu Virbel] + +- Merge pull request #46 from matham/master. [akshayaurora] + + Add compat module, remove decoding of strings in notification + +- Explicitly declare HANDLE since ctypes in py3 doesn't seem to have it. + [Matthew Einhorn] + +- Make py3 compat. [Matthew Einhorn] + +- Decode textinput input before sending to notification. [Matthew + Einhorn] + +- Add compat module, remove decoding of strings in notification, do + direct inmport instead of relative import. [Matthew Einhorn] + +- Removed unused import. [Ben Rousch] + +- Merge pull request #6 from inclement/vibrate. [Alexander Taylor] + + Added Vibrator facade and android implementation + +- Renamed vibrate_pattern to just pattern. [Alexander Taylor] + +- Fixed typo in vibrator proxy comment. [Alexander Taylor] + +- Added vibrator example and buildozer.spec. [Alexander Taylor] + +- Added AndroidVibrator implementation. [Alexander Taylor] + +- Added Vibrator facade. [Alexander Taylor] + +- Merge pull request #18 from matham/ctypes-notify-window. [Ben Rousch] + + Changes notify to use ctypes instead of win32gui so we could use unicode. + +- Use unique id. [Matthew Einhorn] + +- Use count instead of self to generate different classes. [Matthew + Einhorn] + +- Add notify_close method and always display some systray icon. [Matthew + Einhorn] + +- Update to use ctypes instead of win32gui so we could use unicode. + [Matthew Einhorn] + +- Add windows ctypes api defs file. [Matthew Einhorn] + +- Merge pull request #39 from trivedigaurav/accelerometer_example. [Ben + Rousch] + + Created an accelerometer example. Uses garden graph to plot the values + +- Removed unused variable. [gtrivedi] + +- Added a simple accelerometer example. [gtrivedi] + +- Corrected README for buildozer commands. [gtrivedi] + +- Updated README to reflect path changes. [gtrivedi] + +- Moved the graph example to examples/accelerometer/using_graph. + [gtrivedi] + +- Updated README to include information about garden.graph. [gtrivedi] + +- Added screenshot of the app running on Android 4.3. [gtrivedi] + +- Added README on how to build. [gtrivedi] + +- Cleaned up a bit. Newlines. Removed dangling commented lines. + [gtrivedi] + +- Fixed ordering in the try catch block. It fails at the correct line in + the try block if the accelerometer is not present. [gtrivedi] + +- Fixed android permissions. Doesn't require any. [gtrivedi] + +- Changed popup error message to state that the problem is with the + current platform. [gtrivedi] + +- Removed a test file not a part of the example. [gtrivedi] + +- Created an accelerometer example. Uses garden graph to plot the + values. [gtrivedi] + +- Added examples README. [Ben Rousch] + +- Merge pull request #38 from trivedigaurav/tts_example. [Ben Rousch] + + Shows an error popup if there is no TTS + +- Added newlines at the end of files. [gtrivedi] + +- Shows an error popup if there is no TTS capability. [gtrivedi] + +- Merge pull request #37 from trivedigaurav/tts_example. [akshayaurora] + + Text to Speech Example + +- Removed an empty line. [gtrivedi] + +- Created an example application for the text to speech (tts) facade. + [gtrivedi] + +- Merge pull request #11 from kivy/notification_windows_icon. + [akshayaurora] + + User-specified icon support for Windows notifications + +- Notification: introduce `timeout` [qua-non] + +- User-specified icon support for Windows notifications. [Ben Rousch] + +- Merge pull request #15 from voen/patch-1. [Ben Rousch] + + readme typo corrected + +- Readme typo corrected. [voen] + +- Merge pull request #10 from kivy/dbus_notify. [akshayaurora] + + Introduce dbus notification + +- Removed attempts at using app_icon in Android notification. [Ben + Rousch] + +- Improve notification example. [qua-non] + +- Partial example notification example. [Ben Rousch] + +- Introduce dbus notification. [qua-non] + +- Fix plyer android.activity import. [Mathieu Virbel] + +- Fixed whereis_exe for windows. Fixed espeak TTS for windows. [Ben + Rousch] + +- Merge pull request #5 from inclement/sendemail. [Mathieu Virbel] + + Added an email facade and basic android implementation + +- Removed unnecessary import from email. [Alexander Taylor] + +- Pep8 fixes. [Alexander Taylor] + +- Added email facade example. [Alexander Taylor] + +- Added email to README. [Alexander Taylor] + +- Completed email facade and android implementation. [Alexander Taylor] + +- Added email facade. [Alexander Taylor] + +- Added simple buildozer.spec for gps example. [Alexander Taylor] + +- Add missing super() constructor in IosAccelerometer. [Mathieu Virbel] + +- Ios: add support for accelerometer on iOS (and motivate brousch again) + [Mathieu Virbel] + +- Add MANIFEST to include LICENSE and README. bump to 1.1.2. [Mathieu + Virbel] + +- Bump to 1.1.1. [Mathieu Virbel] + +- Fix setup for pip. [Mathieu Virbel] + +- Update readme. [Mathieu Virbel] + +- Setup.py: fix readme. [Mathieu Virbel] + +- Update readme. [Mathieu Virbel] + +- Gps: add versionadded. [Mathieu Virbel] + +- Fix documentation version. [Mathieu Virbel] + +- Gps: update documentation. [Mathieu Virbel] + +- Update setup.py to correctly include win. [Mathieu Virbel] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Add a basic grid to show supported features per os. [tshirtman] + +- Custom icon specification for android notification. [Ben Rousch] + +- Even more elegant Android notification code. [Ben Rousch] + +- Less hacky getPackageName from Android. [Ben Rousch] + +- Fixed hasattr bug. Added default icon support for Android + notifications. [Ben Rousch] + +- PEPed up balloontip. Fixed no 2nd notify bug. Fixed blocking + notification bug. [Ben Rousch] + +- Added notifications for Linux via notify-send. [Ben Rousch] + +- Fixed name of Windows platform, fixed Windows notify. [Ben Rousch] + +- Add GPS/android support for plyer. [Mathieu Virbel] + +- Add setup.py. [Mathieu Virbel] + +- Plyer is now under MIT license. [Mathieu Virbel] + +- Fixed incorrect Android tTS return type. [Ben Rousch] + +- Merge pull request #1 from kivy/tts. [Ben Rousch] + + TTS! + +- Changed NotImplemented exception to NotImplementedError. [Ben Rousch] + +- Added missing () to TTS returns. [Ben Rousch] + +- Fixed missed TextToSpeech, deleted old files. [Ben Rousch] + +- Changed TextToSpeech to TTS. Returning TTS instead of raising + NotImplemented. [Ben Rousch] + +- Added text_to_speech for Android, Linux, OSX, and Windows. [Ben + Rousch] + +- Ensure the documentation will find plyer. [Mathieu Virbel] + +- Rework how implementation works, and start documentation. [Mathieu + Virbel] + +- First version of plyer, including accelerometer (android), camera + (android) and notification (android, osx). api is not stabilized. + [Mathieu Virbel] + +- Merge branch 'master' of github.com:kivy/plyer. [tshirtman] + + Conflicts: + readme.md + +- Update readme.md. [Gabriel Pettier] + +- Rename to plyer, and uses plateform() from kivy utils. [tshirtman] + +- Add android/desktop/ios modules, and auto import from them. + [tshirtman] + +- Initial commit, created simple readme. [tshirtman] + + diff --git a/external/plyer/LICENSE b/external/plyer/LICENSE new file mode 100644 index 0000000..b336a3a --- /dev/null +++ b/external/plyer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2015 Kivy Team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/external/plyer/README.rst b/external/plyer/README.rst new file mode 100644 index 0000000..56860db --- /dev/null +++ b/external/plyer/README.rst @@ -0,0 +1,38 @@ +Plyer +===== + +Plyer is a platform-independent api to use features commonly found on various +platforms, notably mobile ones, in Python. + +How +--- + +Plyer tries not to reinvent the wheel, and will call for external libraries to +implement the api in the easiest way, depending on the current platform. + +- on python-for-android, pyjnius is used +- on kivy-ios, pyobjus is used +- on windows/mac/linux, commonly found libraries and programs will be used + +Support +------- + +================================== ============= ============= === ======= === ===== +Platform Android < 4.0 Android > 4.0 iOS Windows OSX Linux +================================== ============= ============= === ======= === ===== +Accelerometer X X X X X +Camera (taking picture) X X +GPS X X X +Notifications X X X X X +Text to speech X X X X X X +Email (open mail client) X X X X X X +Vibrator X X +Sms (send messages) X X +Compass X X X +Unique ID (IMEI or SN) X X X X X X +Gyroscope X X X +Battery X X X X X X +Native file chooser X X X +Orientation X X +Audio recording X X +================================== ============= ============= === ======= === ===== diff --git a/external/plyer/__init__.py b/external/plyer/__init__.py new file mode 100644 index 0000000..f8e35e5 --- /dev/null +++ b/external/plyer/__init__.py @@ -0,0 +1,63 @@ +''' +Plyer +===== + +''' + +__all__ = ('accelerometer', 'audio', 'battery', 'camera', 'compass', 'email', + 'filechooser', 'gps', 'gyroscope', 'irblaster', 'orientation', + 'notification', 'sms', 'tts', 'uniqueid', 'vibrator') + +__version__ = '1.2.4' + + +from plyer import facades +from plyer.utils import Proxy + +#: Accelerometer proxy to :class:`plyer.facades.Accelerometer` +accelerometer = Proxy('accelerometer', facades.Accelerometer) + +#: Audio proxy to :class:`plyer.facades.Audio` +audio = Proxy('audio', facades.Audio) + +#: Battery proxy to :class:`plyer.facades.Battery` +battery = Proxy('battery', facades.Battery) + +#: Compass proxy to :class:`plyer.facades.Compass` +compass = Proxy('compass', facades.Compass) + +#: Camera proxy to :class:`plyer.facades.Camera` +camera = Proxy('camera', facades.Camera) + +#: Email proxy to :class:`plyer.facades.Email` +email = Proxy('email', facades.Email) + +#: FileChooser proxy to :class:`plyer.facades.FileChooser` +filechooser = Proxy('filechooser', facades.FileChooser) + +#: GPS proxy to :class:`plyer.facades.GPS` +gps = Proxy('gps', facades.GPS) + +#: Gyroscope proxy to :class:`plyer.facades.Gyroscope` +gyroscope = Proxy('gyroscope', facades.Gyroscope) + +#: IrBlaster proxy to :class:`plyer.facades.IrBlaster` +irblaster = Proxy('irblaster', facades.IrBlaster) + +#: Orientation proxy to :class:`plyer.facades.Orientation` +orientation = Proxy('orientation', facades.Orientation) + +#: Notification proxy to :class:`plyer.facades.Notification` +notification = Proxy('notification', facades.Notification) + +#: Sms proxy to :class:`plyer.facades.Sms` +sms = Proxy('sms', facades.Sms) + +#: TTS proxy to :class:`plyer.facades.TTS` +tts = Proxy('tts', facades.TTS) + +#: UniqueID proxy to :class:`plyer.facades.UniqueID` +uniqueid = Proxy('uniqueid', facades.UniqueID) + +#: Vibrator proxy to :class:`plyer.facades.Vibrator` +vibrator = Proxy('vibrator', facades.Vibrator) diff --git a/external/plyer/__init__.pyc b/external/plyer/__init__.pyc Binary files differnew file mode 100644 index 0000000..d78a630 --- /dev/null +++ b/external/plyer/__init__.pyc diff --git a/external/plyer/compat.py b/external/plyer/compat.py new file mode 100644 index 0000000..fee64da --- /dev/null +++ b/external/plyer/compat.py @@ -0,0 +1,34 @@ +''' +Compatibility module for Python 2.7 and > 3.3 +============================================= +''' + +__all__ = ('PY2', 'string_types', 'queue', 'iterkeys', + 'itervalues', 'iteritems') + +import sys +try: + import queue +except ImportError: + import Queue as queue + +#: True if Python 2 intepreter is used +PY2 = sys.version_info[0] == 2 + +#: String types that can be used for checking if a object is a string +string_types = None +text_type = None +if PY2: + string_types = basestring + text_type = unicode +else: + string_types = text_type = str + +if PY2: + iterkeys = lambda d: d.iterkeys() + itervalues = lambda d: d.itervalues() + iteritems = lambda d: d.iteritems() +else: + iterkeys = lambda d: iter(d.keys()) + itervalues = lambda d: iter(d.values()) + iteritems = lambda d: iter(d.items()) diff --git a/external/plyer/facades/__init__.py b/external/plyer/facades/__init__.py new file mode 100644 index 0000000..e5560c0 --- /dev/null +++ b/external/plyer/facades/__init__.py @@ -0,0 +1,28 @@ +''' +Facades +======= + +Interface of all the features available. + +''' + +__all__ = ('Accelerometer', 'Audio', 'Battery', 'Camera', 'Compass', 'Email', + 'FileChooser', 'GPS', 'Gyroscope', 'IrBlaster', 'Orientation', + 'Notification', 'Sms', 'TTS', 'UniqueID', 'Vibrator') + +from plyer.facades.accelerometer import Accelerometer +from plyer.facades.audio import Audio +from plyer.facades.battery import Battery +from plyer.facades.camera import Camera +from plyer.facades.compass import Compass +from plyer.facades.email import Email +from plyer.facades.filechooser import FileChooser +from plyer.facades.gps import GPS +from plyer.facades.gyroscope import Gyroscope +from plyer.facades.irblaster import IrBlaster +from plyer.facades.orientation import Orientation +from plyer.facades.notification import Notification +from plyer.facades.sms import Sms +from plyer.facades.tts import TTS +from plyer.facades.uniqueid import UniqueID +from plyer.facades.vibrator import Vibrator diff --git a/external/plyer/facades/__init__.pyc b/external/plyer/facades/__init__.pyc Binary files differnew file mode 100644 index 0000000..8058b7a --- /dev/null +++ b/external/plyer/facades/__init__.pyc diff --git a/external/plyer/facades/accelerometer.py b/external/plyer/facades/accelerometer.py new file mode 100644 index 0000000..e8146bf --- /dev/null +++ b/external/plyer/facades/accelerometer.py @@ -0,0 +1,36 @@ +class Accelerometer(object): + '''Accelerometer facade. + ''' + + @property + def acceleration(self): + '''Property that returns values of the current acceleration + sensors, as a (x, y, z) tuple. Returns (None, None, None) + if no data is currently available. + ''' + return self.get_acceleration() + + def enable(self): + '''Activate the accelerometer sensor. Throws an error if the + hardware is not available or not implemented on. + ''' + self._enable() + + def disable(self): + '''Disable the accelerometer sensor. + ''' + self._disable() + + def get_acceleration(self): + return self._get_acceleration() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_acceleration(self): + raise NotImplementedError() diff --git a/external/plyer/facades/accelerometer.pyc b/external/plyer/facades/accelerometer.pyc Binary files differnew file mode 100644 index 0000000..c646a98 --- /dev/null +++ b/external/plyer/facades/accelerometer.pyc diff --git a/external/plyer/facades/audio.py b/external/plyer/facades/audio.py new file mode 100644 index 0000000..5678841 --- /dev/null +++ b/external/plyer/facades/audio.py @@ -0,0 +1,56 @@ +class Audio(object): + '''Audio Facade. + + Used for recording audio. + Use method `start` to start record and `stop` for stop recording. + To hear what you have just recorded, use method `play`. + + `status` will tell you about current job of the Audio. + + Default path for recording is set in platform implementation. + + .. note:: + On Android the `RECORD_AUDIO` permission is needed. + ''' + + state = 'ready' + _file_path = '' + + def __init__(self, file_path): + super(Audio, self).__init__() + self._file_path = file_path + + def start(self): + '''Start record.''' + self._start() + self.state = 'recording' + + def _start(self): + raise NotImplementedError() + + def stop(self): + '''Stop record.''' + self._stop() + self.state = 'ready' + + def _stop(self): + raise NotImplementedError() + + def play(self): + '''Play current recording.''' + self._play() + self.state = 'playing' + + def _play(self): + raise NotImplementedError() + + @property + def file_path(self): + return self._file_path + + @file_path.setter + def file_path(self, location): + '''Location of the recording.''' + assert isinstance(location, (basestring, unicode)), \ + 'Location must be string or unicode' + self._file_path = location diff --git a/external/plyer/facades/audio.pyc b/external/plyer/facades/audio.pyc Binary files differnew file mode 100644 index 0000000..9b79fcf --- /dev/null +++ b/external/plyer/facades/audio.pyc diff --git a/external/plyer/facades/battery.py b/external/plyer/facades/battery.py new file mode 100644 index 0000000..7cd48c7 --- /dev/null +++ b/external/plyer/facades/battery.py @@ -0,0 +1,22 @@ +class Battery(object): + '''Battery info facade.''' + + @property + def status(self): + '''Property that contains a dict with the following fields: + * **isCharging** *(bool)*: Battery is charging + * **percentage** *(float)*: Battery charge remaining + + .. warning:: + If any of the fields is not readable, it is set as + None. + ''' + return self.get_state() + + def get_state(self): + return self._get_state() + + #private + + def _get_state(self): + raise NotImplementedError() diff --git a/external/plyer/facades/battery.pyc b/external/plyer/facades/battery.pyc Binary files differnew file mode 100644 index 0000000..07f25d7 --- /dev/null +++ b/external/plyer/facades/battery.pyc diff --git a/external/plyer/facades/camera.py b/external/plyer/facades/camera.py new file mode 100644 index 0000000..5300ede --- /dev/null +++ b/external/plyer/facades/camera.py @@ -0,0 +1,43 @@ +class Camera(object): + '''Camera facade. + ''' + + def take_picture(self, filename, on_complete): + '''Ask the OS to capture a picture, and store it at filename. + + When the capture is done, on_complete will be called with the filename + as an argument. If the callback returns True, the filename will be + unlinked. + + :param filename: Name of the image file + :param on_complete: Callback that will be called when the operation is + done + + :type filename: str + :type on_complete: callable + ''' + self._take_picture(filename=filename, on_complete=on_complete) + + def take_video(self, filename, on_complete): + '''Ask the OS to capture a video, and store it at filename. + + When the capture is done, on_complete will be called with the filename + as an argument. If the callback returns True, the filename will be + unlinked. + + :param filename: Name of the video file + :param on_complete: Callback that will be called when the operation is + done + + :type filename: str + :type on_complete: callable + ''' + self._take_video(filename=filename, on_complete=on_complete) + + # private + + def _take_picture(self, **kwargs): + raise NotImplementedError() + + def _take_video(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/camera.pyc b/external/plyer/facades/camera.pyc Binary files differnew file mode 100644 index 0000000..5146deb --- /dev/null +++ b/external/plyer/facades/camera.pyc diff --git a/external/plyer/facades/compass.py b/external/plyer/facades/compass.py new file mode 100644 index 0000000..aed4bbf --- /dev/null +++ b/external/plyer/facades/compass.py @@ -0,0 +1,37 @@ +class Compass(object): + '''Compass facade. + + .. versionadded:: 1.2.0 + ''' + + @property + def orientation(self): + '''Property that returns values of the current compass + (magnetic field) sensors, as a (x, y, z) tuple. + Returns (None, None, None) if no data is currently available. + ''' + return self.get_orientation() + + def enable(self): + '''Activate the compass sensor. + ''' + self._enable() + + def disable(self): + '''Disable the compass sensor. + ''' + self._disable() + + def get_orientation(self): + return self._get_orientation() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_orientation(self): + raise NotImplementedError() diff --git a/external/plyer/facades/compass.pyc b/external/plyer/facades/compass.pyc Binary files differnew file mode 100644 index 0000000..af2e244 --- /dev/null +++ b/external/plyer/facades/compass.pyc diff --git a/external/plyer/facades/email.py b/external/plyer/facades/email.py new file mode 100644 index 0000000..6056a16 --- /dev/null +++ b/external/plyer/facades/email.py @@ -0,0 +1,23 @@ +class Email(object): + '''Email facade.''' + + def send(self, recipient=None, subject=None, text=None, + create_chooser=None): + '''Open an email client message send window, prepopulated with the + given arguments. + + :param recipient: Recipient of the message (str) + :param subject: Subject of the message (str) + :param text: Main body of the message (str) + :param create_chooser: Whether to display a program chooser to + handle the message (bool) + + .. note:: create_chooser is only supported on Android + ''' + self._send(recipient=recipient, subject=subject, text=text, + create_chooser=create_chooser) + + # private + + def _send(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/email.pyc b/external/plyer/facades/email.pyc Binary files differnew file mode 100644 index 0000000..b1ee2bf --- /dev/null +++ b/external/plyer/facades/email.pyc diff --git a/external/plyer/facades/filechooser.py b/external/plyer/facades/filechooser.py new file mode 100644 index 0000000..040e9bb --- /dev/null +++ b/external/plyer/facades/filechooser.py @@ -0,0 +1,53 @@ +class FileChooser(object): + '''Native filechooser dialog facade. + + open_file, save_file and choose_dir accept a number of arguments + listed below. They return either a list of paths (normally + absolute), or None if no file was selected or the operation was + canceled and no result is available. + + Arguments: + * **path** *(string or None)*: a path that will be selected + by default, or None + * **multiple** *(bool)*: True if you want the dialog to + allow multiple file selection. (Note: Windows doesn't + support multiple directory selection) + * **filters** *(iterable)*: either a list of wildcard patterns + or of sequences that contain the name of the filter and any + number of wildcards that will be grouped under that name + (e.g. [["Music", "*mp3", "*ogg", "*aac"], "*jpg", "*py"]) + * **preview** *(bool)*: True if you want the file chooser to + show a preview of the selected file, if supported by the + back-end. + * **title** *(string or None)*: The title of the file chooser + window, or None for the default title. + * **icon** *(string or None)*: Path to the icon of the file + chooser window (where supported), or None for the back-end's + default. + * **show_hidden** *(bool)*: Force showing hidden files (currently + supported only on Windows) + + Important: these methods will return only after user interaction. + Use threads or you will stop the mainloop if your app has one. + ''' + + def _file_selection_dialog(self, **kwargs): + raise NotImplementedError() + + def open_file(self, *args, **kwargs): + """Open the file chooser in "open" mode. + """ + return self._file_selection_dialog(mode="open", *args, **kwargs) + + def save_file(self, *args, **kwargs): + """Open the file chooser in "save" mode. Confirmation will be asked + when a file with the same name already exists. + """ + return self._file_selection_dialog(mode="save", *args, **kwargs) + + def choose_dir(self, *args, **kwargs): + """Open the directory chooser. Note that on Windows this is very + limited. Consider writing your own chooser if you target that + platform and are planning on using unsupported features. + """ + return self._file_selection_dialog(mode="dir", *args, **kwargs) diff --git a/external/plyer/facades/filechooser.pyc b/external/plyer/facades/filechooser.pyc Binary files differnew file mode 100644 index 0000000..70c0f61 --- /dev/null +++ b/external/plyer/facades/filechooser.pyc diff --git a/external/plyer/facades/gps.py b/external/plyer/facades/gps.py new file mode 100644 index 0000000..09f7e4b --- /dev/null +++ b/external/plyer/facades/gps.py @@ -0,0 +1,67 @@ +class GPS(object): + '''GPS facade. + + .. versionadded:: 1.1 + + You need to set a `on_location` callback with the :meth:`configure` method. + This callback will receive a couple of keywords / values, that might be + different depending of their availability on the targeted platform. + Lat and lon are always available. + + - lat: latitude of the last location, in degrees + - lon: longitude of the last location, in degrees + - speed: speed of the user, in meters/second over ground + - bearing: bearing in degrees + - altitude: altitude in meters above the sea level + + Here is an example of the usage of gps:: + + from plyer import gps + + def print_locations(**kwargs): + print 'lat: {lat}, lon: {lon}'.format(**kwargs) + + gps.configure(on_location=print_locations) + gps.start() + # later + gps.stop() + ''' + + def configure(self, on_location, on_status=None): + '''Configure the GPS object. This method should be called before + :meth:`start`. + + :param on_location: Function to call when receiving a new location + :param on_status: Function to call when a status message is received + :type on_location: callable, multiples keys/value will be passed. + :type on_status: callable, args are "message-type", "status" + + .. warning:: + + The `on_location` and `on_status` callables might be called from + another thread than the thread used for creating the GPS object. + ''' + self.on_location = on_location + self.on_status = on_status + self._configure() + + def start(self): + '''Start the GPS location updates. + ''' + self._start() + + def stop(self): + '''Stop the GPS location updates. + ''' + self._stop() + + # private + + def _configure(self): + raise NotImplementedError() + + def _start(self): + raise NotImplementedError() + + def _stop(self): + raise NotImplementedError() diff --git a/external/plyer/facades/gps.pyc b/external/plyer/facades/gps.pyc Binary files differnew file mode 100644 index 0000000..175cf9e --- /dev/null +++ b/external/plyer/facades/gps.pyc diff --git a/external/plyer/facades/gyroscope.py b/external/plyer/facades/gyroscope.py new file mode 100644 index 0000000..31ec1ae --- /dev/null +++ b/external/plyer/facades/gyroscope.py @@ -0,0 +1,37 @@ +class Gyroscope(object): + '''Gyroscope facade. + + .. versionadded:: 1.2.0 + ''' + + @property + def orientation(self): + '''Property that returns values of the current Gyroscope sensors, as + a (x, y, z) tuple. Returns (None, None, None) if no data is currently + available. + ''' + return self.get_orientation() + + def enable(self): + '''Activate the Gyroscope sensor. + ''' + self._enable() + + def disable(self): + '''Disable the Gyroscope sensor. + ''' + self._disable() + + def get_orientation(self): + return self._get_orientation() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_orientation(self): + raise NotImplementedError() diff --git a/external/plyer/facades/gyroscope.pyc b/external/plyer/facades/gyroscope.pyc Binary files differnew file mode 100644 index 0000000..c6989c6 --- /dev/null +++ b/external/plyer/facades/gyroscope.pyc diff --git a/external/plyer/facades/irblaster.py b/external/plyer/facades/irblaster.py new file mode 100644 index 0000000..0e15a07 --- /dev/null +++ b/external/plyer/facades/irblaster.py @@ -0,0 +1,59 @@ +class IrBlaster(object): + '''Infrared blaster facade.''' + + @staticmethod + def periods_to_microseconds(frequency, pattern): + '''Convert a pattern from period counts to microseconds. + ''' + period = 1000000. / frequency + return [period * x for x in pattern] + + @staticmethod + def microseconds_to_periods(frequency, pattern): + '''Convert a pattern from microseconds to period counts. + ''' + period = 1000000. / frequency + return [x / period for x in pattern] + + @property + def frequencies(self): + '''Property which contains a list of frequency ranges + supported by the device in the form: + + [(from1, to1), + (from2, to2), + ... + (fromN, toN)] + ''' + return self.get_frequencies() + + def get_frequencies(self): + return self._get_frequencies() + + def _get_frequencies(self): + raise NotImplementedError() + + def transmit(self, frequency, pattern, mode='period'): + '''Transmit an IR sequence. + + :parameters: + `frequency`: int + Carrier frequency for the IR transmission. + `pattern`: list[int] + Burst pair pattern to transmit. + `mode`: str, defaults to 'period' + Specifies the format of the pattern values. + Can be 'period' or 'microseconds'. + ''' + return self._transmit(frequency, pattern, mode) + + def _transmit(self, frequency, pattern, mode): + raise NotImplementedError() + + def exists(self): + '''Check if the device has an infrared emitter. + ''' + return self._exists() + + def _exists(self): + raise NotImplementedError() diff --git a/external/plyer/facades/irblaster.pyc b/external/plyer/facades/irblaster.pyc Binary files differnew file mode 100644 index 0000000..74c9d71 --- /dev/null +++ b/external/plyer/facades/irblaster.pyc diff --git a/external/plyer/facades/notification.py b/external/plyer/facades/notification.py new file mode 100644 index 0000000..3a477e9 --- /dev/null +++ b/external/plyer/facades/notification.py @@ -0,0 +1,26 @@ +class Notification(object): + '''Notification facade. + ''' + + def notify(self, title='', message='', app_name='', app_icon='', + timeout=10): + '''Send a notification. + + :param title: Title of the notification + :param message: Message of the notification + :param app_name: Name of the app launching this notification + :param app_icon: Icon to be displayed along with the message + :param timeout: time to display the message for, defaults to 10 + :type title: str + :type message: str + :type app_name: str + :type app_icon: str + :type timeout: int + ''' + self._notify(title=title, message=message, app_icon=app_icon, + app_name=app_name, timeout=timeout) + + # private + + def _notify(self, **kwargs): + raise NotImplementedError("No usable implementation found!") diff --git a/external/plyer/facades/notification.pyc b/external/plyer/facades/notification.pyc Binary files differnew file mode 100644 index 0000000..8a31ae3 --- /dev/null +++ b/external/plyer/facades/notification.pyc diff --git a/external/plyer/facades/orientation.py b/external/plyer/facades/orientation.py new file mode 100644 index 0000000..e58e723 --- /dev/null +++ b/external/plyer/facades/orientation.py @@ -0,0 +1,45 @@ +class Orientation(object): + '''Orientation facade. + + .. note:: + These settings are generally guidelines, the operating + system may choose to ignore them, or they may be overridden by + other system components. + + .. versionadded:: 1.2.4 + ''' + + def set_landscape(self, reverse=False): + '''Rotate the app to a landscape orientation. + + :param reverse: If True, uses the opposite of the natural + orientation. + ''' + self._set_landscape(reverse=reverse) + + def _set_landscape(self, **kwargs): + raise NotImplementedError() + + def set_portrait(self, reverse=False): + '''Rotate the app to a portrait orientation. + + :param reverse: If True, uses the opposite of the natural + orientation. + ''' + self._set_portrait(reverse=reverse) + + def _set_portrait(self, **kwargs): + raise NotImplementedError() + + def set_sensor(self, mode='any'): + '''Rotate freely following sensor information from the device. + + :param mode: The rotation mode, should be one of 'any' (rotate + to any orientation), 'landscape' (choose nearest + landscape mode) or 'portrait' (choose nearest + portrait mode). Defaults to 'any'. + ''' + self._set_sensor(mode=mode) + + def _set_sensor(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/orientation.pyc b/external/plyer/facades/orientation.pyc Binary files differnew file mode 100644 index 0000000..23deae8 --- /dev/null +++ b/external/plyer/facades/orientation.pyc diff --git a/external/plyer/facades/sms.py b/external/plyer/facades/sms.py new file mode 100644 index 0000000..ce4f299 --- /dev/null +++ b/external/plyer/facades/sms.py @@ -0,0 +1,19 @@ +class Sms(object): + '''Sms facade. + + .. note:: + + On Android your app needs the SEND_SMS permission in order to + send sms messages. + + .. versionadded:: 1.2.0 + + ''' + + def send(self, recipient, message): + self._send(recipient=recipient, message=message) + + # private + + def _send(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/sms.pyc b/external/plyer/facades/sms.pyc Binary files differnew file mode 100644 index 0000000..7294345 --- /dev/null +++ b/external/plyer/facades/sms.pyc diff --git a/external/plyer/facades/tts.py b/external/plyer/facades/tts.py new file mode 100644 index 0000000..91c1a76 --- /dev/null +++ b/external/plyer/facades/tts.py @@ -0,0 +1,16 @@ +class TTS(object): + '''TextToSpeech facade. + ''' + + def speak(self, message=''): + '''Use text to speech capabilities to speak the message. + + :param message: What to speak + :type message: str + ''' + self._speak(message=message) + + # private + + def _speak(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/tts.pyc b/external/plyer/facades/tts.pyc Binary files differnew file mode 100644 index 0000000..d5d395f --- /dev/null +++ b/external/plyer/facades/tts.pyc diff --git a/external/plyer/facades/uniqueid.py b/external/plyer/facades/uniqueid.py new file mode 100644 index 0000000..d916f01 --- /dev/null +++ b/external/plyer/facades/uniqueid.py @@ -0,0 +1,29 @@ +class UniqueID(object): + '''UniqueID facade. + + Returns the following depending on the platform: + + * **Android**: Android ID + * **OS X**: Serial number of the device + * **Linux**: Serial number using lshw + * **Windows**: MachineGUID from regkey + + .. versionadded:: 1.2.0 + + .. versionchanged:: 1.2.4 + On Android returns Android ID instead of IMEI. + ''' + + @property + def id(self): + '''Property that returns the unique id of the platform. + ''' + return self.get_uid() + + def get_uid(self): + return self._get_uid() + + # private + + def _get_uid(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/uniqueid.pyc b/external/plyer/facades/uniqueid.pyc Binary files differnew file mode 100644 index 0000000..2d4c2ff --- /dev/null +++ b/external/plyer/facades/uniqueid.pyc diff --git a/external/plyer/facades/vibrator.py b/external/plyer/facades/vibrator.py new file mode 100644 index 0000000..94fe9aa --- /dev/null +++ b/external/plyer/facades/vibrator.py @@ -0,0 +1,53 @@ +class Vibrator(object): + '''Vibration facade. + + .. note:: + On Android your app needs the VIBRATE permission to + access the vibrator. + ''' + + def vibrate(self, time=1): + '''Ask the vibrator to vibrate for the given period. + + :param time: Time to vibrate for, in seconds. Default is 1. + ''' + self._vibrate(time=time) + + def _vibrate(self, **kwargs): + raise NotImplementedError() + + def pattern(self, pattern=(0, 1), repeat=-1): + '''Ask the vibrator to vibrate with the given pattern, with an + optional repeat. + + :param pattern: Pattern to vibrate with. Should be a list of + times in seconds. The first number is how long to wait + before vibrating, and subsequent numbers are times to + vibrate and not vibrate alternately. + Defaults to ``[0, 1]``. + + :param repeat: Index at which to repeat the pattern. When the + vibration pattern reaches this index, it will start again + from the beginning. Defaults to ``-1``, which means no + repeat. + ''' + self._pattern(pattern=pattern, repeat=repeat) + + def _pattern(self, **kwargs): + raise NotImplementedError() + + def exists(self): + '''Check if the device has a vibrator. Returns True or + False. + ''' + return self._exists() + + def _exists(self, **kwargs): + raise NotImplementedError() + + def cancel(self): + '''Cancels any current vibration, and stops the vibrator.''' + self._cancel() + + def _cancel(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/vibrator.pyc b/external/plyer/facades/vibrator.pyc Binary files differnew file mode 100644 index 0000000..cec7d7f --- /dev/null +++ b/external/plyer/facades/vibrator.pyc diff --git a/external/plyer/platforms/__init__.py b/external/plyer/platforms/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/__init__.py diff --git a/external/plyer/platforms/android/__init__.py b/external/plyer/platforms/android/__init__.py new file mode 100644 index 0000000..6b565a3 --- /dev/null +++ b/external/plyer/platforms/android/__init__.py @@ -0,0 +1,12 @@ +from os import environ +from jnius import autoclass + +ANDROID_VERSION = autoclass('android.os.Build$VERSION') +SDK_INT = ANDROID_VERSION.SDK_INT + +if 'PYTHON_SERVICE_ARGUMENT' in environ: + PythonService = autoclass('org.renpy.android.PythonService') + activity = PythonService.mService +else: + PythonActivity = autoclass('org.renpy.android.PythonActivity') + activity = PythonActivity.mActivity diff --git a/external/plyer/platforms/android/accelerometer.py b/external/plyer/platforms/android/accelerometer.py new file mode 100644 index 0000000..af07c52 --- /dev/null +++ b/external/plyer/platforms/android/accelerometer.py @@ -0,0 +1,74 @@ +''' +Android accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class AccelerometerSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super(AccelerometerSensorListener, self).__init__() + self.SensorManager = cast('android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE)) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_ACCELEROMETER) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidAccelerometer(Accelerometer): + def __init__(self): + super(AndroidAccelerometer, self).__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listener = AccelerometerSensorListener() + self.listener.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listener.disable() + del self.listener + + def _get_acceleration(self): + if (self.bState): + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super(self.__class__, self).__del__() + + +def instance(): + return AndroidAccelerometer() diff --git a/external/plyer/platforms/android/audio.py b/external/plyer/platforms/android/audio.py new file mode 100644 index 0000000..2115f19 --- /dev/null +++ b/external/plyer/platforms/android/audio.py @@ -0,0 +1,58 @@ +from jnius import autoclass + +from plyer.facades.audio import Audio + +# Recorder Classes +MediaRecorder = autoclass('android.media.MediaRecorder') +AudioSource = autoclass('android.media.MediaRecorder$AudioSource') +OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat') +AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder') + +# Player Classes +MediaPlayer = autoclass('android.media.MediaPlayer') + + +class AndroidAudio(Audio): + '''Audio for android. + + For recording audio we use MediaRecorder Android class. + For playing audio we use MediaPlayer Android class. + ''' + + def __init__(self, file_path=None): + default_path = '/sdcard/testrecorder.3gp' + super(AndroidAudio, self).__init__(file_path or default_path) + + self._recorder = None + self._player = None + + def _start(self): + self._recorder = MediaRecorder() + self._recorder.setAudioSource(AudioSource.DEFAULT) + self._recorder.setOutputFormat(OutputFormat.DEFAULT) + self._recorder.setAudioEncoder(AudioEncoder.DEFAULT) + self._recorder.setOutputFile(self.file_path) + + self._recorder.prepare() + self._recorder.start() + + def _stop(self): + if self._recorder: + self._recorder.stop() + self._recorder.release() + self._recorder = None + + if self._player: + self._player.stop() + self._player.release() + self._player = None + + def _play(self): + self._player = MediaPlayer() + self._player.setDataSource(self.file_path) + self._player.prepare() + self._player.start() + + +def instance(): + return AndroidAudio() diff --git a/external/plyer/platforms/android/battery.py b/external/plyer/platforms/android/battery.py new file mode 100644 index 0000000..2ade1d2 --- /dev/null +++ b/external/plyer/platforms/android/battery.py @@ -0,0 +1,34 @@ +from jnius import autoclass, cast +from plyer.platforms.android import activity +from plyer.facades import Battery + +Intent = autoclass('android.content.Intent') +BatteryManager = autoclass('android.os.BatteryManager') +IntentFilter = autoclass('android.content.IntentFilter') + + +class AndroidBattery(Battery): + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + + batteryStatus = cast('android.content.Intent', + activity.registerReceiver(None, ifilter)) + + query = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + isCharging = (query == BatteryManager.BATTERY_STATUS_CHARGING or + query == BatteryManager.BATTERY_STATUS_FULL) + + level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + percentage = level / float(scale) + + status['isCharging'] = isCharging + status['percentage'] = percentage + + return status + + +def instance(): + return AndroidBattery() diff --git a/external/plyer/platforms/android/camera.py b/external/plyer/platforms/android/camera.py new file mode 100644 index 0000000..344296d --- /dev/null +++ b/external/plyer/platforms/android/camera.py @@ -0,0 +1,59 @@ +import android +import android.activity +from os import unlink +from jnius import autoclass, cast +from plyer.facades import Camera +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +PythonActivity = autoclass('org.renpy.android.PythonActivity') +MediaStore = autoclass('android.provider.MediaStore') +Uri = autoclass('android.net.Uri') + + +class AndroidCamera(Camera): + + def _take_picture(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + android.activity.unbind(on_activity_result=self._on_activity_result) + android.activity.bind(on_activity_result=self._on_activity_result) + intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + uri = Uri.parse('file://' + filename) + parcelable = cast('android.os.Parcelable', uri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + activity.startActivityForResult(intent, 0x123) + + def _take_video(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + android.activity.unbind(on_activity_result=self._on_activity_result) + android.activity.bind(on_activity_result=self._on_activity_result) + intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) + uri = Uri.parse('file://' + filename) + parcelable = cast('android.os.Parcelable', uri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + + # 0 = low quality, suitable for MMS messages, + # 1 = high quality + intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1) + activity.startActivityForResult(intent, 0x123) + + def _on_activity_result(self, requestCode, resultCode, intent): + if requestCode != 0x123: + return + android.activity.unbind(on_activity_result=self._on_activity_result) + if self.on_complete(self.filename): + self._unlink(self.filename) + + def _unlink(self, fn): + try: + unlink(fn) + except: + pass + + +def instance(): + return AndroidCamera() diff --git a/external/plyer/platforms/android/compass.py b/external/plyer/platforms/android/compass.py new file mode 100644 index 0000000..7fb19d6 --- /dev/null +++ b/external/plyer/platforms/android/compass.py @@ -0,0 +1,74 @@ +''' +Android Compass +--------------------- +''' + +from plyer.facades import Compass +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class MagneticFieldSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super(MagneticFieldSensorListener, self).__init__() + self.SensorManager = cast('android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE)) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_MAGNETIC_FIELD) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidCompass(Compass): + def __init__(self): + super(AndroidCompass, self).__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listener = MagneticFieldSensorListener() + self.listener.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listener.disable() + del self.listener + + def _get_orientation(self): + if (self.bState): + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super(self.__class__, self).__del__() + + +def instance(): + return AndroidCompass() diff --git a/external/plyer/platforms/android/email.py b/external/plyer/platforms/android/email.py new file mode 100644 index 0000000..79923e4 --- /dev/null +++ b/external/plyer/platforms/android/email.py @@ -0,0 +1,40 @@ +from jnius import autoclass, cast +from plyer.facades import Email +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +AndroidString = autoclass('java.lang.String') + + +class AndroidEmail(Email): + def _send(self, **kwargs): + intent = Intent(Intent.ACTION_SEND) + intent.setType('text/plain') + + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + create_chooser = kwargs.get('create_chooser') + + if recipient: + intent.putExtra(Intent.EXTRA_EMAIL, [recipient]) + if subject: + android_subject = cast('java.lang.CharSequence', + AndroidString(subject)) + intent.putExtra(Intent.EXTRA_SUBJECT, android_subject) + if text: + android_text = cast('java.lang.CharSequence', + AndroidString(text)) + intent.putExtra(Intent.EXTRA_TEXT, android_text) + + if create_chooser: + chooser_title = cast('java.lang.CharSequence', + AndroidString('Send message with:')) + activity.startActivity(Intent.createChooser(intent, + chooser_title)) + else: + activity.startActivity(intent) + + +def instance(): + return AndroidEmail() diff --git a/external/plyer/platforms/android/gps.py b/external/plyer/platforms/android/gps.py new file mode 100644 index 0000000..fbe580f --- /dev/null +++ b/external/plyer/platforms/android/gps.py @@ -0,0 +1,79 @@ +''' +Android GPS +----------- +''' + +from plyer.facades import GPS +from plyer.platforms.android import activity +from jnius import autoclass, java_method, PythonJavaClass + +Looper = autoclass('android.os.Looper') +LocationManager = autoclass('android.location.LocationManager') +Context = autoclass('android.content.Context') + + +class _LocationListener(PythonJavaClass): + __javainterfaces__ = ['android/location/LocationListener'] + + def __init__(self, root): + self.root = root + super(_LocationListener, self).__init__() + + @java_method('(Landroid/location/Location;)V') + def onLocationChanged(self, location): + self.root.on_location( + lat=location.getLatitude(), + lon=location.getLongitude(), + speed=location.getSpeed(), + bearing=location.getBearing(), + altitude=location.getAltitude()) + + @java_method('(Ljava/lang/String;)V') + def onProviderEnabled(self, status): + if self.root.on_status: + self.root.on_status('provider-enabled', status) + + @java_method('(Ljava/lang/String;)V') + def onProviderDisabled(self, status): + if self.root.on_status: + self.root.on_status('provider-disabled', status) + + @java_method('(Ljava/lang/String;ILandroid/os/Bundle;)V') + def onStatusChanged(self, provider, status, extras): + if self.root.on_status: + s_status = 'unknown' + if status == 0x00: + s_status = 'out-of-service' + elif status == 0x01: + s_status = 'temporarily-unavailable' + elif status == 0x02: + s_status = 'available' + self.root.on_status('provider-status', '{}: {}'.format( + provider, s_status)) + + +class AndroidGPS(GPS): + + def _configure(self): + if not hasattr(self, '_location_manager'): + self._location_manager = activity.getSystemService( + Context.LOCATION_SERVICE) + self._location_listener = _LocationListener(self) + + def _start(self): + # XXX defaults should be configurable by the user, later + providers = self._location_manager.getProviders(False).toArray() + for provider in providers: + self._location_manager.requestLocationUpdates( + provider, + 1000, # minTime, in milliseconds + 1, # minDistance, in meters + self._location_listener, + Looper.getMainLooper()) + + def _stop(self): + self._location_manager.removeUpdates(self._location_listener) + + +def instance(): + return AndroidGPS() diff --git a/external/plyer/platforms/android/gyroscope.py b/external/plyer/platforms/android/gyroscope.py new file mode 100644 index 0000000..58747d7 --- /dev/null +++ b/external/plyer/platforms/android/gyroscope.py @@ -0,0 +1,74 @@ +''' +Android Gyroscope +--------------------- +''' + +from plyer.facades import Gyroscope +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class GyroscopeSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super(GyroscopeSensorListener, self).__init__() + self.SensorManager = cast('android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE)) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_GYROSCOPE) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidGyroscope(Gyroscope): + def __init__(self): + super(AndroidGyroscope, self).__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listener = GyroscopeSensorListener() + self.listener.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listener.disable() + del self.listener + + def _get_orientation(self): + if (self.bState): + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super(self.__class__, self).__del__() + + +def instance(): + return AndroidGyroscope() diff --git a/external/plyer/platforms/android/irblaster.py b/external/plyer/platforms/android/irblaster.py new file mode 100644 index 0000000..6c44717 --- /dev/null +++ b/external/plyer/platforms/android/irblaster.py @@ -0,0 +1,54 @@ +from jnius import autoclass + +from plyer.facades import IrBlaster +from plyer.platforms.android import activity, SDK_INT, ANDROID_VERSION + +if SDK_INT >= 19: + Context = autoclass('android.content.Context') + ir_manager = activity.getSystemService(Context.CONSUMER_IR_SERVICE) +else: + ir_manager = None + + +class AndroidIrBlaster(IrBlaster): + def _exists(self): + if ir_manager and ir_manager.hasIrEmitter(): + return True + return False + + @property + def multiply_pulse(self): + '''Android 4.4.3+ uses microseconds instead of period counts + ''' + return not (SDK_INT == 19 and + int(str(ANDROID_VERSION.RELEASE).rsplit('.', 1)[-1]) < 3) + + def _get_frequencies(self): + if not ir_manager: + return None + + if hasattr(self, '_frequencies'): + return self._frequencies + + ir_frequencies = ir_manager.getCarrierFrequencies() + if not ir_frequencies: + return [] + + frequencies = [] + for freqrange in ir_frequencies: + freq = (freqrange.getMinFrequency(), freqrange.getMaxFrequency()) + frequencies.append(freq) + + self._frequencies = frequencies + return frequencies + + def _transmit(self, frequency, pattern, mode): + if self.multiply_pulse and mode == 'period': + pattern = self.periods_to_microseconds(frequency, pattern) + elif not self.multiply_pulse and mode == 'microseconds': + pattern = self.microseconds_to_periods(frequency, pattern) + ir_manager.transmit(frequency, pattern) + + +def instance(): + return AndroidIrBlaster() diff --git a/external/plyer/platforms/android/notification.py b/external/plyer/platforms/android/notification.py new file mode 100644 index 0000000..bfc3a25 --- /dev/null +++ b/external/plyer/platforms/android/notification.py @@ -0,0 +1,37 @@ +from jnius import autoclass +from plyer.facades import Notification +from plyer.platforms.android import activity, SDK_INT + +AndroidString = autoclass('java.lang.String') +Context = autoclass('android.content.Context') +NotificationBuilder = autoclass('android.app.Notification$Builder') +Drawable = autoclass("{}.R$drawable".format(activity.getPackageName())) + + +class AndroidNotification(Notification): + def _get_notification_service(self): + if not hasattr(self, '_ns'): + self._ns = activity.getSystemService(Context.NOTIFICATION_SERVICE) + return self._ns + + def _notify(self, **kwargs): + icon = getattr(Drawable, kwargs.get('icon_android', 'icon')) + noti = NotificationBuilder(activity) + noti.setContentTitle(AndroidString( + kwargs.get('title').encode('utf-8'))) + noti.setContentText(AndroidString( + kwargs.get('message').encode('utf-8'))) + noti.setSmallIcon(icon) + noti.setAutoCancel(True) + + if SDK_INT >= 16: + noti = noti.build() + else: + noti = noti.getNotification() + + self._get_notification_service().notify(0, noti) + + +def instance(): + return AndroidNotification() + diff --git a/external/plyer/platforms/android/orientation.py b/external/plyer/platforms/android/orientation.py new file mode 100644 index 0000000..e98e34d --- /dev/null +++ b/external/plyer/platforms/android/orientation.py @@ -0,0 +1,43 @@ +from jnius import autoclass, cast +from plyer.platforms.android import activity +from plyer.facades import Orientation + +ActivityInfo = autoclass('android.content.pm.ActivityInfo') + + +class AndroidOrientation(Orientation): + + def _set_landscape(self, **kwargs): + reverse = kwargs.get('reverse') + if reverse: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + + def _set_portrait(self, **kwargs): + reverse = kwargs.get('reverse') + if reverse: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + + def _set_sensor(self, **kwargs): + mode = kwargs.get('mode') + + if mode == 'any': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR) + elif mode == 'landscape': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) + elif mode == 'portrait': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) + + +def instance(): + return AndroidOrientation() diff --git a/external/plyer/platforms/android/sms.py b/external/plyer/platforms/android/sms.py new file mode 100644 index 0000000..8650968 --- /dev/null +++ b/external/plyer/platforms/android/sms.py @@ -0,0 +1,25 @@ +''' +Android SMS +----------- +''' + +from jnius import autoclass +from plyer.facades import Sms + +SmsManager = autoclass('android.telephony.SmsManager') + + +class AndroidSms(Sms): + + def _send(self, **kwargs): + sms = SmsManager.getDefault() + + recipient = kwargs.get('recipient') + message = kwargs.get('message') + + if sms: + sms.sendTextMessage(recipient, None, message, None, None) + + +def instance(): + return AndroidSms() diff --git a/external/plyer/platforms/android/tts.py b/external/plyer/platforms/android/tts.py new file mode 100644 index 0000000..eae2974 --- /dev/null +++ b/external/plyer/platforms/android/tts.py @@ -0,0 +1,24 @@ +from time import sleep +from jnius import autoclass +from plyer.facades import TTS +from plyer.platforms.android import activity + +Locale = autoclass('java.util.Locale') +TextToSpeech = autoclass('android.speech.tts.TextToSpeech') + + +class AndroidTextToSpeech(TTS): + def _speak(self, **kwargs): + tts = TextToSpeech(activity, None) + tts.setLanguage(Locale.US) # TODO: locale specification as option + retries = 0 # First try rarely succeeds due to some timing issue + while retries < 100 and \ + tts.speak(kwargs.get('message').encode('utf-8'), + TextToSpeech.QUEUE_FLUSH, None) == -1: + # -1 indicates error. Let's wait and then try again + sleep(0.1) + retries += 1 + + +def instance(): + return AndroidTextToSpeech() diff --git a/external/plyer/platforms/android/uniqueid.py b/external/plyer/platforms/android/uniqueid.py new file mode 100644 index 0000000..b8561de --- /dev/null +++ b/external/plyer/platforms/android/uniqueid.py @@ -0,0 +1,16 @@ +from jnius import autoclass +from plyer.platforms.android import activity +from plyer.facades import UniqueID + +Secure = autoclass('android.provider.Settings$Secure') + + +class AndroidUniqueID(UniqueID): + + def _get_uid(self): + return Secure.getString(activity.getContentResolver(), + Secure.ANDROID_ID) + + +def instance(): + return AndroidUniqueID() diff --git a/external/plyer/platforms/android/vibrator.py b/external/plyer/platforms/android/vibrator.py new file mode 100644 index 0000000..c28fe8e --- /dev/null +++ b/external/plyer/platforms/android/vibrator.py @@ -0,0 +1,48 @@ +'''Implementation Vibrator for Android.''' + +from jnius import autoclass +from plyer.facades import Vibrator +from plyer.platforms.android import activity +from plyer.platforms.android import SDK_INT + +Context = autoclass('android.content.Context') +vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) + + +class AndroidVibrator(Vibrator): + '''Android Vibrator class. + + Supported features: + * vibrate for some period of time. + * vibrate from given pattern. + * cancel vibration. + * check whether Vibrator exists. + ''' + + def _vibrate(self, time=None, **kwargs): + if vibrator: + vibrator.vibrate(int(1000 * time)) + + def _pattern(self, pattern=None, repeat=None, **kwargs): + pattern = [int(1000 * time) for time in pattern] + + if vibrator: + vibrator.vibrate(pattern, repeat) + + def _exists(self, **kwargs): + if SDK_INT >= 11: + return vibrator.hasVibrator() + elif activity.getSystemService(Context.VIBRATOR_SERVICE) is None: + raise NotImplementedError() + return True + + def _cancel(self, **kwargs): + vibrator.cancel() + + +def instance(): + '''Returns Vibrator with android features. + + :return: instance of class AndroidVibrator + ''' + return AndroidVibrator() diff --git a/external/plyer/platforms/ios/__init__.py b/external/plyer/platforms/ios/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/ios/__init__.py diff --git a/external/plyer/platforms/ios/accelerometer.py b/external/plyer/platforms/ios/accelerometer.py new file mode 100644 index 0000000..bf1ef02 --- /dev/null +++ b/external/plyer/platforms/ios/accelerometer.py @@ -0,0 +1,34 @@ +''' +iOS accelerometer +----------------- + +Taken from: http://pyobjus.readthedocs.org/en/latest/pyobjus_ios.html \ + #accessing-accelerometer +''' + +from plyer.facades import Accelerometer +from pyobjus import autoclass + + +class IosAccelerometer(Accelerometer): + + def __init__(self): + super(IosAccelerometer, self).__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setAccelerometerUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startAccelerometer() + + def _disable(self): + self.bridge.stopAccelerometer() + + def _get_acceleration(self): + return ( + self.bridge.ac_x, + self.bridge.ac_y, + self.bridge.ac_z) + + +def instance(): + return IosAccelerometer() diff --git a/external/plyer/platforms/ios/battery.py b/external/plyer/platforms/ios/battery.py new file mode 100644 index 0000000..55aa2c6 --- /dev/null +++ b/external/plyer/platforms/ios/battery.py @@ -0,0 +1,36 @@ +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework +from plyer.facades import Battery + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIDevice = autoclass('UIDevice') + + +class iOSBattery(Battery): + def __init__(self): + super(iOSBattery, self).__init__() + self.device = UIDevice.currentDevice() + + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + if(not self.device.batteryMonitoringEnabled): + self.device.setBatteryMonitoringEnabled_(True) + + if self.device.batteryState == 0: + isCharging = None + elif self.device.batteryState == 2: + isCharging = True + else: + isCharging = False + + percentage = self.device.batteryLevel * 100. + + status['isCharging'] = isCharging + status['percentage'] = percentage + + return status + + +def instance(): + return iOSBattery() diff --git a/external/plyer/platforms/ios/compass.py b/external/plyer/platforms/ios/compass.py new file mode 100644 index 0000000..6e5c935 --- /dev/null +++ b/external/plyer/platforms/ios/compass.py @@ -0,0 +1,31 @@ +''' +iOS Compass +--------------------- +''' + +from plyer.facades import Compass +from pyobjus import autoclass + + +class IosCompass(Compass): + + def __init__(self): + super(IosCompass, self).__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setMagnetometerUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startMagnetometer() + + def _disable(self): + self.bridge.stopMagnetometer() + + def _get_orientation(self): + return ( + self.bridge.mg_x, + self.bridge.mg_y, + self.bridge.mg_z) + + +def instance(): + return IosCompass() diff --git a/external/plyer/platforms/ios/email.py b/external/plyer/platforms/ios/email.py new file mode 100644 index 0000000..7e55e4e --- /dev/null +++ b/external/plyer/platforms/ios/email.py @@ -0,0 +1,41 @@ +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + +from plyer.facades import Email +from pyobjus import autoclass, objc_str +from pyobjus.dylib_manager import load_framework + +load_framework('/System/Library/Frameworks/UIKit.framework') + +NSURL = autoclass('NSURL') +NSString = autoclass('NSString') +UIApplication = autoclass('UIApplication') + + +class iOSXEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + nsurl = NSURL.alloc().initWithString_(objc_str(uri)) + + UIApplication.sharedApplication().openURL_(nsurl) + + +def instance(): + return iOSXEmail() diff --git a/external/plyer/platforms/ios/gps.py b/external/plyer/platforms/ios/gps.py new file mode 100644 index 0000000..4d6d665 --- /dev/null +++ b/external/plyer/platforms/ios/gps.py @@ -0,0 +1,45 @@ +''' +iOS GPS +----------- +''' + +from pyobjus import autoclass, protocol +from pyobjus.dylib_manager import load_framework +from plyer.facades import GPS + +load_framework('/System/Library/Frameworks/CoreLocation.framework') +CLLocationManager = autoclass('CLLocationManager') + + +class IosGPS(GPS): + def _configure(self): + if not hasattr(self, '_location_manager'): + self._location_manager = CLLocationManager.alloc().init() + + def _start(self): + self._location_manager.delegate = self + + self._location_manager.requestWhenInUseAuthorization() + # NSLocationWhenInUseUsageDescription key must exist in Info.plist + # file. When the authorization prompt is displayed your app goes + # into pause mode and if your app doesn't support background mode + # it will crash. + self._location_manager.startUpdatingLocation() + + def _stop(self): + self._location_manager.stopUpdatingLocation() + + @protocol('CLLocationManagerDelegate') + def locationManager_didUpdateLocations_(self, manager, locations): + location = manager.location + + self.on_location( + lat=location.coordinate.a, + lon=location.coordinate.b, + speed=location.speed, + bearing=location.course, + altitude=location.altitude) + + +def instance(): + return IosGPS() diff --git a/external/plyer/platforms/ios/gyroscope.py b/external/plyer/platforms/ios/gyroscope.py new file mode 100644 index 0000000..e8b93cf --- /dev/null +++ b/external/plyer/platforms/ios/gyroscope.py @@ -0,0 +1,31 @@ +''' +iOS Gyroscope +--------------------- +''' + +from plyer.facades import Gyroscope +from pyobjus import autoclass + + +class IosGyroscope(Gyroscope): + + def __init__(self): + super(IosGyroscope, self).__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setGyroscopeUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startGyroscope() + + def _disable(self): + self.bridge.stopGyroscope() + + def _get_orientation(self): + return ( + self.bridge.gy_x, + self.bridge.gy_y, + self.bridge.gy_z) + + +def instance(): + return IosGyroscope() diff --git a/external/plyer/platforms/ios/tts.py b/external/plyer/platforms/ios/tts.py new file mode 100644 index 0000000..a711483 --- /dev/null +++ b/external/plyer/platforms/ios/tts.py @@ -0,0 +1,35 @@ +from pyobjus import autoclass, objc_str +from pyobjus.dylib_manager import load_framework + +from plyer.facades import TTS + +load_framework('/System/Library/Frameworks/AVFoundation.framework') +AVSpeechUtterance = autoclass('AVSpeechUtterance') +AVSpeechSynthesizer = autoclass('AVSpeechSynthesizer') +AVSpeechSynthesisVoice = autoclass('AVSpeechSynthesisVoice') + + +class iOSTextToSpeech(TTS): + def __init__(self): + super(iOSTextToSpeech, self).__init__() + self.synth = AVSpeechSynthesizer.alloc().init() + self.voice = None + + def _set_locale(self, locale="en-US"): + self.voice = AVSpeechSynthesisVoice.voiceWithLanguage_(objc_str(locale)) + + def _speak(self, **kwargs): + message = kwargs.get('message') + + if(not self.voice): + self._set_locale() + + utterance = \ + AVSpeechUtterance.speechUtteranceWithString_(objc_str(message)) + + utterance.voice = self.voice + self.synth.speakUtterance_(utterance) + + +def instance(): + return iOSTextToSpeech() diff --git a/external/plyer/platforms/ios/uniqueid.py b/external/plyer/platforms/ios/uniqueid.py new file mode 100644 index 0000000..1587f4b --- /dev/null +++ b/external/plyer/platforms/ios/uniqueid.py @@ -0,0 +1,17 @@ +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework +from plyer.facades import UniqueID + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIDevice = autoclass('UIDevice') + + +class iOSUniqueID(UniqueID): + + def _get_uid(self): + uuid = UIDevice.currentDevice().identifierForVendor.UUIDString() + return uuid.UTF8String() + + +def instance(): + return iOSUniqueID() diff --git a/external/plyer/platforms/linux/__init__.py b/external/plyer/platforms/linux/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/linux/__init__.py diff --git a/external/plyer/platforms/linux/accelerometer.py b/external/plyer/platforms/linux/accelerometer.py new file mode 100644 index 0000000..7272c33 --- /dev/null +++ b/external/plyer/platforms/linux/accelerometer.py @@ -0,0 +1,36 @@ +''' +Linux accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +import os +import glob +import re + + +class LinuxAccelerometer(Accelerometer): + + def _enable(self): + pass + + def _disable(self): + pass + + def _get_acceleration(self): + try: + pos = glob.glob("/sys/devices/platform/*/position")[0] + except IndexError: + raise Exception('Could not enable accelerometer!') + + with open(pos, "r") as p: + t = p.read() + coords = re.findall(r"[-]?\d+\.?\d*", t) + # Apparently the acceleration on sysfs goes from -1000 to 1000. + # I divide it by 100 to make it equivalent to Android. + # The negative is because the coordinates are inverted on Linux + return [float(i) / -100 for i in coords] + + +def instance(): + return LinuxAccelerometer() diff --git a/external/plyer/platforms/linux/battery.py b/external/plyer/platforms/linux/battery.py new file mode 100644 index 0000000..0cdb763 --- /dev/null +++ b/external/plyer/platforms/linux/battery.py @@ -0,0 +1,46 @@ +from subprocess import Popen, PIPE +from plyer.facades import Battery +from plyer.utils import whereis_exe + +from os import environ + + +class LinuxBattery(Battery): + def _get_state(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + status = {"isCharging": None, "percentage": None} + + # We are supporting only one battery now + dev = "/org/freedesktop/UPower/device/battery_BAT0" + upower_process = Popen(["upower", "-d", dev], + stdout=PIPE) + output = upower_process.communicate()[0] + + environ['LANG'] = old_lang + + if not output: + return status + + power_supply = percentage = None + for l in output.splitlines(): + if 'power supply' in l: + power_supply = l.rpartition(':')[-1].strip() + if 'percentage' in l: + percentage = float(l.rpartition(':')[-1].strip()[:-1]) + + if(power_supply): + status['isCharging'] = power_supply != "yes" + + status['percentage'] = percentage + + return status + + +def instance(): + import sys + if whereis_exe('upower'): + return LinuxBattery() + sys.stderr.write("upower not found.") + return Battery() diff --git a/external/plyer/platforms/linux/email.py b/external/plyer/platforms/linux/email.py new file mode 100644 index 0000000..ceb5497 --- /dev/null +++ b/external/plyer/platforms/linux/email.py @@ -0,0 +1,36 @@ +import subprocess +from urllib import quote +try: + from urllib.parse import quote +except ImportError: + from urllib import quote +from plyer.utils import whereis_exe + + +class LinuxEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + subprocess.Popen(["xdg-open", uri]) + + +def instance(): + import sys + if whereis_exe('xdg-open'): + return LinuxEmail() + sys.stderr.write("xdg-open not found.") + return Email() diff --git a/external/plyer/platforms/linux/filechooser.py b/external/plyer/platforms/linux/filechooser.py new file mode 100644 index 0000000..545487b --- /dev/null +++ b/external/plyer/platforms/linux/filechooser.py @@ -0,0 +1,249 @@ +''' +Linux file chooser +------------------ +''' + +from plyer.facades import FileChooser +from distutils.spawn import find_executable as which +import os +import subprocess as sp +import time + + +class SubprocessFileChooser(object): + '''A file chooser implementation that allows using + subprocess back-ends. + Normally you only need to override _gen_cmdline, executable, + separator and successretcode. + ''' + + executable = "" + '''The name of the executable of the back-end. + ''' + + separator = "|" + '''The separator used by the back-end. Override this for automatic + splitting, or override _split_output. + ''' + + successretcode = 0 + '''The return code which is returned when the user doesn't close the + dialog without choosing anything, or when the app doesn't crash. + ''' + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + _process = None + + def _run_command(self, cmd): + self._process = sp.Popen(cmd, stdout=sp.PIPE) + while True: + ret = self._process.poll() + if ret is not None: + if ret == self.successretcode: + out = self._process.communicate()[0].strip() + self.selection = self._split_output(out) + return self.selection + else: + return None + time.sleep(0.1) + + def _split_output(self, out): + '''This methods receives the output of the back-end and turns + it into a list of paths. + ''' + return out.split(self.separator) + + def _gen_cmdline(self): + '''Returns the command line of the back-end, based on the current + properties. You need to override this. + ''' + raise NotImplementedError() + + def run(self): + return self._run_command(self._gen_cmdline()) + + +class ZenityFileChooser(SubprocessFileChooser): + '''A FileChooser implementation using Zenity (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + ''' + + executable = "zenity" + separator = "|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [ + which(self.executable), + "--file-selection", + "--confirm-overwrite" + ] + if self.multiple: + cmdline += ["--multiple"] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += [ + "--file-filter", + "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:])) + ] + return cmdline + + +class KDialogFileChooser(SubprocessFileChooser): + '''A FileChooser implementation using KDialog (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + ''' + + executable = "kdialog" + separator = "\n" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [which(self.executable)] + + filt = [] + + for f in self.filters: + if type(f) == str: + filt += [f] + else: + filt += list(f[1:]) + + if self.mode == "dir": + cmdline += [ + "--getexistingdirectory", + (self.path if self.path else os.path.expanduser("~")) + ] + elif self.mode == "save": + cmdline += [ + "--getopenfilename", + (self.path if self.path else os.path.expanduser("~")), + " ".join(filt) + ] + else: + cmdline += [ + "--getopenfilename", + (self.path if self.path else os.path.expanduser("~")), + " ".join(filt) + ] + if self.multiple: + cmdline += ["--multiple", "--separate-output"] + if self.title: + cmdline += ["--title", self.title] + if self.icon: + cmdline += ["--icon", self.icon] + return cmdline + + +class YADFileChooser(SubprocessFileChooser): + '''A NativeFileChooser implementation using YAD (on GNU/Linux). + + Not implemented features: + * show_hidden + ''' + + executable = "yad" + separator = "|?|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [ + which(self.executable), + "--file-selection", + "--confirm-overwrite", + "--geometry", + "800x600+150+150" + ] + if self.multiple: + cmdline += ["--multiple", "--separator", self.separator] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.preview: + cmdline += ["--add-preview"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += [ + "--file-filter", + "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:])) + ] + return cmdline + +CHOOSERS = { + "gnome": ZenityFileChooser, + "kde": KDialogFileChooser, + "yad": YADFileChooser +} + + +class LinuxFileChooser(FileChooser): + '''FileChooser implementation for GNu/Linux. Accepts one additional + keyword argument, *desktop_override*, which, if set, overrides the + back-end that will be used. Set it to "gnome" for Zenity, to "kde" + for KDialog and to "yad" for YAD (Yet Another Dialog). + If set to None or not set, a default one will be picked based on + the running desktop environment and installed back-ends. + ''' + + desktop = None + if str(os.environ.get("XDG_CURRENT_DESKTOP")).lower() == "kde" \ + and which("kdialog"): + desktop = "kde" + elif which("yad"): + desktop = "yad" + elif which("zenity"): + desktop = "gnome" + + def _file_selection_dialog(self, desktop_override=desktop, **kwargs): + if not desktop_override: + desktop_override = desktop + # This means we couldn't find any back-end + if not desktop_override: + raise OSError("No back-end available. Please install one.") + + chooser = CHOOSERS[desktop_override] + c = chooser(**kwargs) + return c.run() + + +def instance(): + return LinuxFileChooser() diff --git a/external/plyer/platforms/linux/notification.py b/external/plyer/platforms/linux/notification.py new file mode 100644 index 0000000..d78f130 --- /dev/null +++ b/external/plyer/platforms/linux/notification.py @@ -0,0 +1,52 @@ +import subprocess +from plyer.facades import Notification +from plyer.utils import whereis_exe + + +class NotifySendNotification(Notification): + ''' Pops up a notification using notify-send + ''' + def _notify(self, **kwargs): + subprocess.call(["notify-send", + kwargs.get('title'), + kwargs.get('message')]) + + +class NotifyDbus(Notification): + ''' notify using dbus interface + ''' + + def _notify(self, **kwargs): + summary = kwargs.get('title', "title") + body = kwargs.get('message', "body") + app_name = kwargs.get('app_name', '') + app_icon = kwargs.get('app_icon', '') + timeout = kwargs.get('timeout', 5000) + actions = kwargs.get('actions', []) + hints = kwargs.get('hints', []) + replaces_id = kwargs.get('replaces_id', 0) + + _bus_name = 'org.freedesktop.Notifications' + _object_path = '/org/freedesktop/Notifications' + _interface_name = _bus_name + + import dbus + session_bus = dbus.SessionBus() + obj = session_bus.get_object(_bus_name, _object_path) + interface = dbus.Interface(obj, _interface_name) + interface.Notify(app_name, replaces_id, app_icon, + summary, body, actions, hints, timeout) + + +def instance(): + import sys + try: + import dbus + return NotifyDbus() + except ImportError: + sys.stderr.write("python-dbus not installed. try:" + "`sudo pip install python-dbus`.") + if whereis_exe('notify-send'): + return NotifySendNotification() + sys.stderr.write("notify-send not found.") + return Notification() diff --git a/external/plyer/platforms/linux/tts.py b/external/plyer/platforms/linux/tts.py new file mode 100644 index 0000000..0a609e1 --- /dev/null +++ b/external/plyer/platforms/linux/tts.py @@ -0,0 +1,25 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class EspeakTextToSpeech(TTS): + ''' Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +class FliteTextToSpeech(TTS): + ''' Speaks using the flite program + ''' + def _speak(self): + subprocess.call(["flite", "-t", kwargs.get('message'), "play"]) + + +def instance(): + if whereis_exe('espeak'): + return EspeakTextToSpeech() + elif whereis_exe('flite'): + return FlitetextToSpeech() + return TTS() diff --git a/external/plyer/platforms/linux/uniqueid.py b/external/plyer/platforms/linux/uniqueid.py new file mode 100644 index 0000000..f7fff89 --- /dev/null +++ b/external/plyer/platforms/linux/uniqueid.py @@ -0,0 +1,30 @@ +from subprocess import Popen, PIPE +from plyer.facades import UniqueID +from plyer.utils import whereis_exe + +from os import environ + + +class LinuxUniqueID(UniqueID): + def _get_uid(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + lshw_process = Popen(["lshw", "-quiet"], stdout=PIPE, stderr=PIPE) + grep_process = Popen(["grep", "-m1", "serial:"], + stdin=lshw_process.stdout, stdout=PIPE) + lshw_process.stdout.close() + output = grep_process.communicate()[0] + environ['LANG'] = old_lang + + if output: + return output.split()[1] + else: + return None + + +def instance(): + import sys + if whereis_exe('lshw'): + return LinuxUniqueID() + sys.stderr.write("lshw not found.") + return UniqueID() diff --git a/external/plyer/platforms/macosx/__init__.py b/external/plyer/platforms/macosx/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/macosx/__init__.py diff --git a/external/plyer/platforms/macosx/accelerometer.py b/external/plyer/platforms/macosx/accelerometer.py new file mode 100644 index 0000000..ec1fe77 --- /dev/null +++ b/external/plyer/platforms/macosx/accelerometer.py @@ -0,0 +1,25 @@ +''' +MacOSX accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +from plyer.platforms.macosx.libs import osx_motion_sensor + + +class OSXAccelerometer(Accelerometer): + def _enable(self): + try: + osx_motion_sensor.get_coord() + except: + raise Exception('Could not enable motion sensor on this macbook!') + + def _disable(self): + pass + + def _get_acceleration(self): + return osx_motion_sensor.get_coord() + + +def instance(): + return OSXAccelerometer() diff --git a/external/plyer/platforms/macosx/battery.py b/external/plyer/platforms/macosx/battery.py new file mode 100644 index 0000000..fe1c525 --- /dev/null +++ b/external/plyer/platforms/macosx/battery.py @@ -0,0 +1,47 @@ +from subprocess import Popen, PIPE +from plyer.facades import Battery +from plyer.utils import whereis_exe + +from os import environ + + +class OSXBattery(Battery): + def _get_state(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + status = {"isCharging": None, "percentage": None} + + ioreg_process = Popen(["ioreg", "-rc", "AppleSmartBattery"], + stdout=PIPE) + output = ioreg_process.communicate()[0] + + environ['LANG'] = old_lang + + if not output: + return status + + IsCharging = MaxCapacity = CurrentCapacity = None + for l in output.splitlines(): + if 'IsCharging' in l: + IsCharging = l.rpartition('=')[-1].strip() + if 'MaxCapacity' in l: + MaxCapacity = float(l.rpartition('=')[-1].strip()) + if 'CurrentCapacity' in l: + CurrentCapacity = float(l.rpartition('=')[-1].strip()) + + if (IsCharging): + status['isCharging'] = IsCharging == "Yes" + + if (CurrentCapacity and MaxCapacity): + status['percentage'] = 100. * CurrentCapacity / MaxCapacity + + return status + + +def instance(): + import sys + if whereis_exe('ioreg'): + return OSXBattery() + sys.stderr.write("ioreg not found.") + return Battery() diff --git a/external/plyer/platforms/macosx/email.py b/external/plyer/platforms/macosx/email.py new file mode 100644 index 0000000..8e29fa9 --- /dev/null +++ b/external/plyer/platforms/macosx/email.py @@ -0,0 +1,38 @@ +import subprocess + +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + +from plyer.facades import Email +from plyer.utils import whereis_exe + + +class MacOSXEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + subprocess.Popen(["open", uri]) + + +def instance(): + import sys + if whereis_exe('open'): + return MacOSXEmail() + sys.stderr.write("open not found.") + return Email() diff --git a/external/plyer/platforms/macosx/filechooser.py b/external/plyer/platforms/macosx/filechooser.py new file mode 100644 index 0000000..5774cc4 --- /dev/null +++ b/external/plyer/platforms/macosx/filechooser.py @@ -0,0 +1,108 @@ +''' +Mac OS X file chooser +--------------------- +''' + +from plyer.facades import FileChooser +from pyobjus import autoclass, objc_arr, objc_str +from pyobjus.dylib_manager import load_framework, INCLUDE + +load_framework(INCLUDE.AppKit) +NSURL = autoclass('NSURL') +NSOpenPanel = autoclass('NSOpenPanel') +NSSavePanel = autoclass('NSSavePanel') +NSOKButton = 1 + + +class MacFileChooser(object): + '''A native implementation of file chooser dialogs using Apple's API + through pyobjus. + + Not implemented features: + * filters (partial, wildcards are converted to extensions if possible. + Pass the Mac-specific "use_extensions" if you can provide + Mac OS X-compatible to avoid automatic conversion) + * multiple (only for save dialog. Available in open dialog) + * icon + * preview + ''' + + mode = "open" + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + use_extensions = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + def run(self): + panel = None + if self.mode in ("open", "dir"): + panel = NSOpenPanel.openPanel() + else: + panel = NSSavePanel.savePanel() + + panel.setCanCreateDirectories_(True) + + panel.setCanChooseDirectories_(self.mode == "dir") + panel.setCanChooseFiles_(self.mode != "dir") + panel.setShowsHiddenFiles_(self.show_hidden) + + if self.title: + panel.setTitle_(objc_str(self.title)) + + if self.mode != "save" and self.multiple: + panel.setAllowsMultipleSelection_(True) + + # Mac OS X does not support wildcards unlike the other platforms. + # This tries to convert wildcards to "extensions" when possible, + # ans sets the panel to also allow other file types, just to be safe. + if len(self.filters) > 0: + filthies = [] + for f in self.filters: + if type(f) == str: + if not self.use_extensions: + if f.strip().endswith("*"): + continue + pystr = f.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + else: + for i in f[1:]: + if not self.use_extensions: + if f.strip().endswith("*"): + continue + pystr = f.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + + ftypes_arr = objc_arr(filthies) + panel.setAllowedFileTypes_(ftypes_arr) + panel.setAllowsOtherFileTypes_(not self.use_extensions) + + if self.path: + url = NSURL.fileURLWithPath_(self.path) + panel.setDirectoryURL_(url) + + if panel.runModal_(): + if self.mode == "save" or not self.multiple: + return [panel.filename().UTF8String()] + else: + return [i.UTF8String() for i in panel.filenames()] + return None + + +class MacOSXFileChooser(FileChooser): + '''FileChooser implementation for Windows, using win3all. + ''' + def _file_selection_dialog(self, **kwargs): + return MacFileChooser(**kwargs).run() + + +def instance(): + return MacOSXFileChooser() diff --git a/external/plyer/platforms/macosx/libs/__init__.py b/external/plyer/platforms/macosx/libs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/macosx/libs/__init__.py diff --git a/external/plyer/platforms/macosx/libs/osx_motion_sensor.py b/external/plyer/platforms/macosx/libs/osx_motion_sensor.py new file mode 100644 index 0000000..cd4fae4 --- /dev/null +++ b/external/plyer/platforms/macosx/libs/osx_motion_sensor.py @@ -0,0 +1,118 @@ +import ctypes +from ctypes import (Structure, cdll, sizeof, + c_int, c_int8, c_int16, c_size_t) +from ctypes.util import find_library +import platform + +ERROR_DICT = { + "0": "IOKit Framework not found, is this OSX?", + "-1": "No SMCMotionSensor service", + "-2": "No sms device", + "-3": "Could not open motion sensor device", + "-4": "Did not receive any coordinates" +} + +IOKit = cdll.LoadLibrary(find_library('IOKit')) + + +class data_structure(Structure): + _fields_ = [ + ('x', c_int16), + ('y', c_int16), + ('z', c_int16), + ('pad', c_int8 * 34), + ] + + +void_p = ctypes.POINTER(ctypes.c_int) + +kern_return_t = ctypes.c_int +KERN_SUCCESS = 0 +KERN_FUNC = 5 # SMC Motion Sensor on MacBook Pro + +mach_port_t = void_p +MACH_PORT_NULL = 0 + +io_object_t = ctypes.c_int +io_object_t = ctypes.c_int +io_iterator_t = void_p +io_object_t = void_p +io_connect_t = void_p +IOItemCount = ctypes.c_uint + +CFMutableDictionaryRef = void_p + + +def is_os_64bit(): + return platform.machine().endswith('64') + + +def read_sms(): + result = kern_return_t() + masterPort = mach_port_t() + + result = IOKit.IOMasterPort(MACH_PORT_NULL, ctypes.byref(masterPort)) + + IOKit.IOServiceMatching.restype = CFMutableDictionaryRef + matchingDictionary = IOKit.IOServiceMatching("SMCMotionSensor") + + iterator = io_iterator_t() + result = IOKit.IOServiceGetMatchingServices(masterPort, matchingDictionary, + ctypes.byref(iterator)) + + if (result != KERN_SUCCESS): + raise ("No coordinates received!") + return -1, None + + IOKit.IOIteratorNext.restype = io_object_t + smsDevice = IOKit.IOIteratorNext(iterator) + + if not smsDevice: + return -2, None + + dataPort = io_connect_t() + result = IOKit.IOServiceOpen(smsDevice, IOKit.mach_task_self(), 0, + ctypes.byref(dataPort)) + + if (result != KERN_SUCCESS): + return -3, None + + inStructure = data_structure() + outStructure = data_structure() + + if(is_os_64bit() or hasattr(IOKit, 'IOConnectCallStructMethod')): + structureInSize = IOItemCount(sizeof(data_structure)) + structureOutSize = c_size_t(sizeof(data_structure)) + + result = IOKit.IOConnectCallStructMethod(dataPort, KERN_FUNC, + ctypes.byref(inStructure), structureInSize, + ctypes.byref(outStructure), ctypes.byref(structureOutSize)) + else: + structureInSize = IOItemCount(sizeof(data_structure)) + structureOutSize = IOItemCount(sizeof(data_structure)) + + result = IOConnectMethodStructureIStructureO(dataPort, KERN_FUNC, + structureInSize, ctypes.byref(structureOutSize), + ctypes.byref(inStructure), ctypes.byref(outStructure)) + + IOKit.IOServiceClose(dataPort) + + if (result != KERN_SUCCESS): + return -4, None + + return 1, outStructure + + +def get_coord(): + if not IOKit: + raise Exception(ERROR_DICT["0"]) + + ret, data = read_sms() + + if (ret > 0): + if(data.x): + return (data.x, data.y, data.z) + else: + return (None, None, None) + else: + raise Exception(ERROR_DICT[str(ret)]) diff --git a/external/plyer/platforms/macosx/notification.py b/external/plyer/platforms/macosx/notification.py new file mode 100644 index 0000000..a52ebe3 --- /dev/null +++ b/external/plyer/platforms/macosx/notification.py @@ -0,0 +1,27 @@ +from plyer.facades import Notification +import Foundation +import objc +import AppKit + + +class OSXNotification(Notification): + def _notify(self, **kwargs): + NSUserNotification = objc.lookUpClass('NSUserNotification') + NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter') + notification = NSUserNotification.alloc().init() + notification.setTitle_(kwargs.get('title').encode('utf-8')) + #notification.setSubtitle_(str(subtitle)) + notification.setInformativeText_(kwargs.get('message').encode('utf-8')) + notification.setSoundName_("NSUserNotificationDefaultSoundName") + #notification.setHasActionButton_(False) + #notification.setOtherButtonTitle_("View") + #notification.setUserInfo_({"action":"open_url", "value":url}) + NSUserNotificationCenter.defaultUserNotificationCenter() \ + .setDelegate_(self) + NSUserNotificationCenter.defaultUserNotificationCenter() \ + .scheduleNotification_(notification) + + +def instance(): + return OSXNotification() + diff --git a/external/plyer/platforms/macosx/tts.py b/external/plyer/platforms/macosx/tts.py new file mode 100644 index 0000000..755e820 --- /dev/null +++ b/external/plyer/platforms/macosx/tts.py @@ -0,0 +1,25 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class NativeSayTextToSpeech(TTS): + '''Speaks using the native OSX 'say' command + ''' + def _speak(self, **kwargs): + subprocess.call(["say", kwargs.get('message')]) + + +class EspeakTextToSpeech(TTS): + '''Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +def instance(): + if whereis_exe('say'): + return NativeSayTextToSpeech() + elif whereis_exe('espeak'): + return EspeakTextToSpeech() + return TTS() diff --git a/external/plyer/platforms/macosx/uniqueid.py b/external/plyer/platforms/macosx/uniqueid.py new file mode 100644 index 0000000..51ba169 --- /dev/null +++ b/external/plyer/platforms/macosx/uniqueid.py @@ -0,0 +1,32 @@ +from subprocess import Popen, PIPE +from plyer.facades import UniqueID +from plyer.utils import whereis_exe + +from os import environ + + +class OSXUniqueID(UniqueID): + def _get_uid(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + ioreg_process = Popen(["ioreg", "-l"], stdout=PIPE) + grep_process = Popen(["grep", "IOPlatformSerialNumber"], + stdin=ioreg_process.stdout, stdout=PIPE) + ioreg_process.stdout.close() + output = grep_process.communicate()[0] + + environ['LANG'] = old_lang + + if output: + return output.split()[3][1:-1] + else: + return None + + +def instance(): + import sys + if whereis_exe('ioreg'): + return OSXUniqueID() + sys.stderr.write("ioreg not found.") + return UniqueID() diff --git a/external/plyer/platforms/win/__init__.py b/external/plyer/platforms/win/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/win/__init__.py diff --git a/external/plyer/platforms/win/battery.py b/external/plyer/platforms/win/battery.py new file mode 100644 index 0000000..04006f8 --- /dev/null +++ b/external/plyer/platforms/win/battery.py @@ -0,0 +1,21 @@ +from plyer.platforms.win.libs.batterystatus import battery_status +from plyer.facades import Battery + + +class WinBattery(Battery): + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + query = battery_status() + + if (not query): + return status + + status["isCharging"] = query["BatteryFlag"] == 8 + status["percentage"] = query["BatteryLifePercent"] + + return status + + +def instance(): + return WinBattery() diff --git a/external/plyer/platforms/win/email.py b/external/plyer/platforms/win/email.py new file mode 100644 index 0000000..e33f5cf --- /dev/null +++ b/external/plyer/platforms/win/email.py @@ -0,0 +1,34 @@ +import os +try: + from urllib.parse import quote +except ImportError: + from urllib import quote +from plyer.facades import Email + + +class WindowsEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + try: + os.startfile(uri) + except WindowsError: + print("Warning: unable to find a program able to send emails.") + + +def instance(): + return WindowsEmail() diff --git a/external/plyer/platforms/win/filechooser.py b/external/plyer/platforms/win/filechooser.py new file mode 100644 index 0000000..4d284dc --- /dev/null +++ b/external/plyer/platforms/win/filechooser.py @@ -0,0 +1,112 @@ +''' +Windows file chooser +-------------------- +''' + +from plyer.facades import FileChooser +from win32com.shell import shell, shellcon +import os +import win32gui +import win32con +import pywintypes + + +class Win32FileChooser(object): + '''A native implementation of NativeFileChooser using the + Win32 API on Windows. + + Not Implemented features (all dialogs): + * preview + * icon + + Not implemented features (in directory selection only - it's limited + by Windows itself): + * preview + * window-icon + * multiple + * show_hidden + * filters + * path + ''' + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + def run(self): + try: + if self.mode != "dir": + args = {} + + if self.path: + args["InitialDir"] = os.path.dirname(self.path) + path = os.path.splitext(os.path.dirname(self.path)) + args["File"] = path[0] + args["DefExt"] = path[1] + args["Title"] = self.title if self.title else "Pick a file..." + args["CustomFilter"] = 'Other file types\x00*.*\x00' + args["FilterIndex"] = 1 + + filters = "" + for f in self.filters: + if type(f) == str: + filters += (f + "\x00") * 2 + else: + filters += f[0] + "\x00" + ";".join(f[1:]) + "\x00" + args["Filter"] = filters + + flags = (win32con.OFN_EXTENSIONDIFFERENT | + win32con.OFN_OVERWRITEPROMPT) + if self.multiple: + flags |= win32con.OFN_ALLOWmultiple | win32con.OFN_EXPLORER + if self.show_hidden: + flags |= win32con.OFN_FORCESHOWHIDDEN + args["Flags"] = flags + + if self.mode == "open": + self.fname, _, _ = win32gui.GetOpenFileNameW(**args) + elif self.mode == "save": + self.fname, _, _ = win32gui.GetSaveFileNameW(**args) + + if self.fname: + if self.multiple: + seq = str(self.fname).split("\x00") + dir_n, base_n = seq[0], seq[1:] + self.selection = [os.path.join(dir_n, i) + for i in base_n] + else: + self.selection = str(self.fname).split("\x00") + else: + # From http://goo.gl/UDqCqo + pidl, display_name, image_list = shell.SHBrowseForFolder( + win32gui.GetDesktopWindow(), + None, + self.title if self.title else "Pick a folder...", + 0, None, None + ) + self.selection = [str(shell.SHGetPathFromIDList(pidl))] + + return self.selection + except (RuntimeError, pywintypes.error): + return None + + +class WinFileChooser(FileChooser): + '''FileChooser implementation for Windows, using win3all. + ''' + + def _file_selection_dialog(self, **kwargs): + return Win32FileChooser(**kwargs).run() + + +def instance(): + return WinFileChooser() diff --git a/external/plyer/platforms/win/libs/__init__.py b/external/plyer/platforms/win/libs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/win/libs/__init__.py diff --git a/external/plyer/platforms/win/libs/balloontip.py b/external/plyer/platforms/win/libs/balloontip.py new file mode 100644 index 0000000..171b25f --- /dev/null +++ b/external/plyer/platforms/win/libs/balloontip.py @@ -0,0 +1,145 @@ +# -- coding: utf-8 -- + +__all__ = ('WindowsBalloonTip', 'balloon_tip') + + +import time +import ctypes +from plyer.platforms.win.libs import win_api_defs +from plyer.compat import PY2 +from threading import RLock + + +WS_OVERLAPPED = 0x00000000 +WS_SYSMENU = 0x00080000 +WM_DESTROY = 2 +CW_USEDEFAULT = 8 + +LR_LOADFROMFILE = 16 +LR_DEFAULTSIZE = 0x0040 +IDI_APPLICATION = 32512 +IMAGE_ICON = 1 + +NOTIFYICON_VERSION_4 = 4 +NIM_ADD = 0 +NIM_MODIFY = 1 +NIM_DELETE = 2 +NIM_SETVERSION = 4 +NIF_MESSAGE = 1 +NIF_ICON = 2 +NIF_TIP = 4 +NIF_INFO = 0x10 +NIIF_USER = 4 +NIIF_LARGE_ICON = 0x20 + + +class WindowsBalloonTip(object): + + _class_atom = 0 + _wnd_class_ex = None + _hwnd = None + _hicon = None + _balloon_icon = None + _notify_data = None + _count = 0 + _lock = RLock() + + @staticmethod + def _get_unique_id(): + WindowsBalloonTip._lock.acquire() + val = WindowsBalloonTip._count + WindowsBalloonTip._count += 1 + WindowsBalloonTip._lock.release() + return val + + def __init__(self, title, message, app_name, app_icon='', timeout=10): + ''' app_icon if given is a icon file. + ''' + + wnd_class_ex = win_api_defs.get_WNDCLASSEXW() + class_name = 'PlyerTaskbar' + str(WindowsBalloonTip._get_unique_id()) + if PY2: + class_name = class_name.decode('utf8') + wnd_class_ex.lpszClassName = class_name + # keep ref to it as long as window is alive + wnd_class_ex.lpfnWndProc =\ + win_api_defs.WindowProc(win_api_defs.DefWindowProcW) + wnd_class_ex.hInstance = win_api_defs.GetModuleHandleW(None) + if wnd_class_ex.hInstance is None: + raise Exception('Could not get windows module instance.') + class_atom = win_api_defs.RegisterClassExW(wnd_class_ex) + if class_atom == 0: + raise Exception('Could not register the PlyerTaskbar class.') + self._class_atom = class_atom + self._wnd_class_ex = wnd_class_ex + + # create window + self._hwnd = win_api_defs.CreateWindowExW(0, class_atom, + '', WS_OVERLAPPED, 0, 0, CW_USEDEFAULT, + CW_USEDEFAULT, None, None, wnd_class_ex.hInstance, None) + if self._hwnd is None: + raise Exception('Could not get create window.') + win_api_defs.UpdateWindow(self._hwnd) + + # load icon + if app_icon: + icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE + hicon = win_api_defs.LoadImageW(None, app_icon, IMAGE_ICON, 0, 0, + icon_flags) + if hicon is None: + raise Exception('Could not load icon {}'. + format(icon_path_name)) + self._balloon_icon = self._hicon = hicon + else: + self._hicon = win_api_defs.LoadIconW(None, + ctypes.cast(IDI_APPLICATION, win_api_defs.LPCWSTR)) + self.notify(title, message, app_name) + if timeout: + time.sleep(timeout) + + def __del__(self): + self.remove_notify() + if self._hicon is not None: + win_api_defs.DestroyIcon(self._hicon) + if self._wnd_class_ex is not None: + win_api_defs.UnregisterClassW(self._class_atom, + self._wnd_class_ex.hInstance) + if self._hwnd is not None: + win_api_defs.DestroyWindow(self._hwnd) + + def notify(self, title, message, app_name): + ''' Displays a balloon in the systray. Can be called multiple times + with different parameter values. + ''' + self.remove_notify() + # add icon and messages to window + hicon = self._hicon + flags = NIF_TIP | NIF_INFO + icon_flag = 0 + if hicon is not None: + flags |= NIF_ICON + # if icon is default app's one, don't display it in message + if self._balloon_icon is not None: + icon_flag = NIIF_USER | NIIF_LARGE_ICON + notify_data = win_api_defs.get_NOTIFYICONDATAW(0, self._hwnd, + id(self), flags, 0, hicon, app_name, 0, 0, message, + NOTIFYICON_VERSION_4, title, icon_flag, win_api_defs.GUID(), + self._balloon_icon) + + self._notify_data = notify_data + if not win_api_defs.Shell_NotifyIconW(NIM_ADD, notify_data): + raise Exception('Shell_NotifyIconW failed.') + if not win_api_defs.Shell_NotifyIconW(NIM_SETVERSION, + notify_data): + raise Exception('Shell_NotifyIconW failed.') + + def remove_notify(self): + '''Removes the notify balloon, if displayed. + ''' + if self._notify_data is not None: + win_api_defs.Shell_NotifyIconW(NIM_DELETE, self._notify_data) + self._notify_data = None + + +def balloon_tip(**kwargs): + WindowsBalloonTip(**kwargs) diff --git a/external/plyer/platforms/win/libs/batterystatus.py b/external/plyer/platforms/win/libs/batterystatus.py new file mode 100644 index 0000000..ddb22cc --- /dev/null +++ b/external/plyer/platforms/win/libs/batterystatus.py @@ -0,0 +1,13 @@ +__all__ = ('battery_status') + + +import ctypes +from plyer.platforms.win.libs import win_api_defs + + +def battery_status(): + status = win_api_defs.SYSTEM_POWER_STATUS() + if not win_api_defs.GetSystemPowerStatus(ctypes.pointer(status)): + raise Exception('Could not get system power status.') + + return dict((field, getattr(status, field)) for field, _ in status._fields_) diff --git a/external/plyer/platforms/win/libs/win_api_defs.py b/external/plyer/platforms/win/libs/win_api_defs.py new file mode 100644 index 0000000..7aed430 --- /dev/null +++ b/external/plyer/platforms/win/libs/win_api_defs.py @@ -0,0 +1,200 @@ +''' Defines ctypes windows api. +''' + +__all__ = ('GUID', 'get_DLLVERSIONINFO', 'MAKEDLLVERULL', + 'get_NOTIFYICONDATAW', 'CreateWindowExW', 'WindowProc', + 'DefWindowProcW', 'get_WNDCLASSEXW', 'GetModuleHandleW', + 'RegisterClassExW', 'UpdateWindow', 'LoadImageW', + 'Shell_NotifyIconW', 'DestroyIcon', 'UnregisterClassW', + 'DestroyWindow', 'LoadIconW') + +import ctypes +from ctypes import Structure, windll, sizeof, POINTER, WINFUNCTYPE +from ctypes.wintypes import (DWORD, HICON, HWND, UINT, WCHAR, WORD, BYTE, + LPCWSTR, INT, LPVOID, HINSTANCE, HMENU, LPARAM, WPARAM, + HBRUSH, HMODULE, ATOM, BOOL, HANDLE) +LRESULT = LPARAM +HRESULT = HANDLE +HCURSOR = HICON + + +class GUID(Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8) + ] + + +class DLLVERSIONINFO(Structure): + _fields_ = [ + ('cbSize', DWORD), + ('dwMajorVersion', DWORD), + ('dwMinorVersion', DWORD), + ('dwBuildNumber', DWORD), + ('dwPlatformID', DWORD), + ] + + +def get_DLLVERSIONINFO(*largs): + version_info = DLLVERSIONINFO(*largs) + version_info.cbSize = sizeof(DLLVERSIONINFO) + return version_info + + +def MAKEDLLVERULL(major, minor, build, sp): + return (major << 48) | (minor << 32) | (build << 16) | sp + + +NOTIFYICONDATAW_fields = [ + ("cbSize", DWORD), + ("hWnd", HWND), + ("uID", UINT), + ("uFlags", UINT), + ("uCallbackMessage", UINT), + ("hIcon", HICON), + ("szTip", WCHAR * 128), + ("dwState", DWORD), + ("dwStateMask", DWORD), + ("szInfo", WCHAR * 256), + ("uVersion", UINT), + ("szInfoTitle", WCHAR * 64), + ("dwInfoFlags", DWORD), + ("guidItem", GUID), + ("hBalloonIcon", HICON), +] + + +class NOTIFYICONDATAW(Structure): + _fields_ = NOTIFYICONDATAW_fields[:] + + +class NOTIFYICONDATAW_V3(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-1] + + +class NOTIFYICONDATAW_V2(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-2] + + +class NOTIFYICONDATAW_V1(Structure): + _fields_ = NOTIFYICONDATAW_fields[:6] + + +NOTIFYICONDATA_V3_SIZE = sizeof(NOTIFYICONDATAW_V3) +NOTIFYICONDATA_V2_SIZE = sizeof(NOTIFYICONDATAW_V2) +NOTIFYICONDATA_V1_SIZE = sizeof(NOTIFYICONDATAW_V1) + + +def get_NOTIFYICONDATAW(*largs): + notify_data = NOTIFYICONDATAW(*largs) + + # get shell32 version to find correct NOTIFYICONDATAW size + DllGetVersion = windll.Shell32.DllGetVersion + DllGetVersion.argtypes = [POINTER(DLLVERSIONINFO)] + DllGetVersion.restype = HRESULT + + version = get_DLLVERSIONINFO() + if DllGetVersion(version): + raise Exception('Cannot get Windows version numbers.') + v = MAKEDLLVERULL(version.dwMajorVersion, version.dwMinorVersion, + version.dwBuildNumber, version.dwPlatformID) + + # from the version info find the NOTIFYICONDATA size + if v >= MAKEDLLVERULL(6, 0, 6, 0): + notify_data.cbSize = sizeof(NOTIFYICONDATAW) + elif v >= MAKEDLLVERULL(6, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V3_SIZE + elif v >= MAKEDLLVERULL(5, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V2_SIZE + else: + notify_data.cbSize = NOTIFYICONDATA_V1_SIZE + return notify_data + + +CreateWindowExW = windll.User32.CreateWindowExW +CreateWindowExW.argtypes = [DWORD, ATOM, LPCWSTR, DWORD, INT, INT, INT, INT, + HWND, HMENU, HINSTANCE, LPVOID] +CreateWindowExW.restype = HWND + +GetModuleHandleW = windll.Kernel32.GetModuleHandleW +GetModuleHandleW.argtypes = [LPCWSTR] +GetModuleHandleW.restype = HMODULE + +WindowProc = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) +DefWindowProcW = windll.User32.DefWindowProcW +DefWindowProcW.argtypes = [HWND, UINT, WPARAM, LPARAM] +DefWindowProcW.restype = LRESULT + + +class WNDCLASSEXW(Structure): + _fields_ = [ + ('cbSize', UINT), + ('style', UINT), + ('lpfnWndProc', WindowProc), + ('cbClsExtra', INT), + ('cbWndExtra', INT), + ('hInstance', HINSTANCE), + ('hIcon', HICON), + ('hCursor', HCURSOR), + ('hbrBackground', HBRUSH), + ('lpszMenuName', LPCWSTR), + ('lpszClassName', LPCWSTR), + ('hIconSm', HICON), + ] + + +def get_WNDCLASSEXW(*largs): + wnd_class = WNDCLASSEXW(*largs) + wnd_class.cbSize = sizeof(WNDCLASSEXW) + return wnd_class + +RegisterClassExW = windll.User32.RegisterClassExW +RegisterClassExW.argtypes = [POINTER(WNDCLASSEXW)] +RegisterClassExW.restype = ATOM + +UpdateWindow = windll.User32.UpdateWindow +UpdateWindow.argtypes = [HWND] +UpdateWindow.restype = BOOL + +LoadImageW = windll.User32.LoadImageW +LoadImageW.argtypes = [HINSTANCE, LPCWSTR, UINT, INT, INT, UINT] +LoadImageW.restype = HANDLE + +Shell_NotifyIconW = windll.Shell32.Shell_NotifyIconW +Shell_NotifyIconW.argtypes = [DWORD, POINTER(NOTIFYICONDATAW)] +Shell_NotifyIconW.restype = BOOL + +DestroyIcon = windll.User32.DestroyIcon +DestroyIcon.argtypes = [HICON] +DestroyIcon.restype = BOOL + +UnregisterClassW = windll.User32.UnregisterClassW +UnregisterClassW.argtypes = [ATOM, HINSTANCE] +UnregisterClassW.restype = BOOL + +DestroyWindow = windll.User32.DestroyWindow +DestroyWindow.argtypes = [HWND] +DestroyWindow.restype = BOOL + +LoadIconW = windll.User32.LoadIconW +LoadIconW.argtypes = [HINSTANCE, LPCWSTR] +LoadIconW.restype = HICON + + +class SYSTEM_POWER_STATUS(ctypes.Structure): + _fields_ = [ + ('ACLineStatus', BYTE), + ('BatteryFlag', BYTE), + ('BatteryLifePercent', BYTE), + ('Reserved1', BYTE), + ('BatteryLifeTime', DWORD), + ('BatteryFullLifeTime', DWORD), + ] + +SystemPowerStatusP = ctypes.POINTER(SYSTEM_POWER_STATUS) + +GetSystemPowerStatus = ctypes.windll.kernel32.GetSystemPowerStatus +GetSystemPowerStatus.argtypes = [SystemPowerStatusP] +GetSystemPowerStatus.restype = BOOL diff --git a/external/plyer/platforms/win/notification.py b/external/plyer/platforms/win/notification.py new file mode 100644 index 0000000..ea46e08 --- /dev/null +++ b/external/plyer/platforms/win/notification.py @@ -0,0 +1,13 @@ +from threading import Thread as thread + +from plyer.facades import Notification +from plyer.platforms.win.libs.balloontip import balloon_tip + + +class WindowsNotification(Notification): + def _notify(self, **kwargs): + thread(target=balloon_tip, kwargs=kwargs).start() + + +def instance(): + return WindowsNotification() diff --git a/external/plyer/platforms/win/tts.py b/external/plyer/platforms/win/tts.py new file mode 100644 index 0000000..e2539c3 --- /dev/null +++ b/external/plyer/platforms/win/tts.py @@ -0,0 +1,16 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class EspeakTextToSpeech(TTS): + ''' Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +def instance(): + if whereis_exe('espeak.exe'): + return EspeakTextToSpeech() + return TTS() diff --git a/external/plyer/platforms/win/uniqueid.py b/external/plyer/platforms/win/uniqueid.py new file mode 100644 index 0000000..bfcf996 --- /dev/null +++ b/external/plyer/platforms/win/uniqueid.py @@ -0,0 +1,21 @@ +try: + import _winreg as regedit +except: + try: + import winreg as regedit + except: + raise NotImplemented() + +from plyer.facades import UniqueID + + +class WinUniqueID(UniqueID): + def _get_uid(self): + hKey = regedit.OpenKey(regedit.HKEY_LOCAL_MACHINE, + r"SOFTWARE\\Microsoft\\Cryptography") + value, _ = regedit.QueryValueEx(hKey, "MachineGuid") + return value + + +def instance(): + return WinUniqueID() diff --git a/external/plyer/tools/pep8checker/pep8.py b/external/plyer/tools/pep8checker/pep8.py new file mode 100644 index 0000000..63a78e2 --- /dev/null +++ b/external/plyer/tools/pep8checker/pep8.py @@ -0,0 +1,1956 @@ +#!/usr/bin/env python +# pep8.py - Check Python source code formatting, according to PEP 8 +# Copyright (C) 2006 Johann C. Rocholl <johann@rocholl.net> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +r""" +Check Python source code formatting, according to PEP 8: +http://www.python.org/dev/peps/pep-0008/ + +For usage and a list of options, try this: +$ python pep8.py -h + +This program and its regression test suite live here: +http://github.com/jcrocholl/pep8 + +Groups of errors and warnings: +E errors +W warnings +100 indentation +200 whitespace +300 blank lines +400 imports +500 line length +600 deprecation +700 statements +900 syntax error + +You can add checks to this program by writing plugins. Each plugin is +a simple function that is called for each line of source code, either +physical or logical. + +Physical line: +- Raw line of text from the input file. + +Logical line: +- Multi-line statements converted to a single line. +- Stripped left and right. +- Contents of strings replaced with 'xxx' of same length. +- Comments removed. + +The check function requests physical or logical lines by the name of +the first argument: + +def maximum_line_length(physical_line) +def extraneous_whitespace(logical_line) +def blank_lines(logical_line, blank_lines, indent_level, line_number) + +The last example above demonstrates how check plugins can request +additional information with extra arguments. All attributes of the +Checker object are available. Some examples: + +lines: a list of the raw lines from the input file +tokens: the tokens that contribute to this logical line +line_number: line number in the input file +blank_lines: blank lines before this one +indent_char: first indentation character in this file (' ' or '\t') +indent_level: indentation (with tabs expanded to multiples of 8) +previous_indent_level: indentation on previous line +previous_logical: previous logical line + +The docstring of each check function shall be the relevant part of +text from PEP 8. It is printed if the user enables --show-pep8. +Several docstrings contain examples directly from the PEP 8 document. + +Okay: spam(ham[1], {eggs: 2}) +E201: spam( ham[1], {eggs: 2}) + +These examples are verified automatically when pep8.py is run with the +--doctest option. You can add examples for your own check functions. +The format is simple: "Okay" or error/warning code followed by colon +and space, the rest of the line is example source code. If you put 'r' +before the docstring, you can use \n for newline, \t for tab and \s +for space. + +""" + +__version__ = '1.3.3' + +import os +import sys +import re +import time +import inspect +import keyword +import tokenize +from optparse import OptionParser +from fnmatch import fnmatch +try: + from ConfigParser import RawConfigParser +except ImportError: + from configparser import RawConfigParser + from io import TextIOWrapper + +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git' +DEFAULT_IGNORE = 'E24' +if sys.platform == 'win32': + DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') +else: + DEFAULT_CONFIG = os.path.join(os.getenv('XDG_CONFIG_HOME') or + os.path.expanduser('~/.config'), 'pep8') +MAX_LINE_LENGTH = 80 +REPORT_FORMAT = { + 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', + 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', +} + + +SINGLETONS = frozenset(['False', 'None', 'True']) +KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS +BINARY_OPERATORS = frozenset([ + '**=', '*=', '+=', '-=', '!=', '<>', + '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=', + '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<']) +UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) +OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS +WHITESPACE = frozenset(' \t') +SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE, + tokenize.INDENT, tokenize.DEDENT]) +BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines'] + +INDENT_REGEX = re.compile(r'([ \t]*)') +RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)') +RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,\s*\w+\s*,\s*\w+') +SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)') +ERRORCODE_REGEX = re.compile(r'[EW]\d{3}') +DOCSTRING_REGEX = re.compile(r'u?r?["\']') +EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') +WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)') +COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)') +COMPARE_TYPE_REGEX = re.compile(r'([=!]=|is|is\s+not)\s*type(?:s\.(\w+)Type' + r'|\(\s*(\(\s*\)|[^)]*[^ )])\s*\))') +KEYWORD_REGEX = re.compile(r'(?:[^\s])(\s*)\b(?:%s)\b(\s*)' % + r'|'.join(KEYWORDS)) +OPERATOR_REGEX = re.compile(r'(?:[^\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)') +LAMBDA_REGEX = re.compile(r'\blambda\b') +HUNK_REGEX = re.compile(r'^@@ -\d+,\d+ \+(\d+),(\d+) @@.*$') + +# Work around Python < 2.6 behaviour, which does not generate NL after +# a comment which is on a line by itself. +COMMENT_WITH_NL = tokenize.generate_tokens(['#\n'].pop).send(None)[1] == '#\n' + + +############################################################################## +# Plugins (check functions) for physical lines +############################################################################## + + +def tabs_or_spaces(physical_line, indent_char): + r""" + Never mix tabs and spaces. + + The most popular way of indenting Python is with spaces only. The + second-most popular way is with tabs only. Code indented with a mixture + of tabs and spaces should be converted to using spaces exclusively. When + invoking the Python command line interpreter with the -t option, it issues + warnings about code that illegally mixes tabs and spaces. When using -tt + these warnings become errors. These options are highly recommended! + + Okay: if a == 0:\n a = 1\n b = 1 + E101: if a == 0:\n a = 1\n\tb = 1 + """ + indent = INDENT_REGEX.match(physical_line).group(1) + for offset, char in enumerate(indent): + if char != indent_char: + return offset, "E101 indentation contains mixed spaces and tabs" + + +def tabs_obsolete(physical_line): + r""" + For new projects, spaces-only are strongly recommended over tabs. Most + editors have features that make this easy to do. + + Okay: if True:\n return + W191: if True:\n\treturn + """ + indent = INDENT_REGEX.match(physical_line).group(1) + if '\t' in indent: + return indent.index('\t'), "W191 indentation contains tabs" + + +def trailing_whitespace(physical_line): + r""" + JCR: Trailing whitespace is superfluous. + FBM: Except when it occurs as part of a blank line (i.e. the line is + nothing but whitespace). According to Python docs[1] a line with only + whitespace is considered a blank line, and is to be ignored. However, + matching a blank line to its indentation level avoids mistakenly + terminating a multi-line statement (e.g. class declaration) when + pasting code into the standard Python interpreter. + + [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines + + The warning returned varies on whether the line itself is blank, for easier + filtering for those who want to indent their blank lines. + + Okay: spam(1) + W291: spam(1)\s + W293: class Foo(object):\n \n bang = 12 + """ + physical_line = physical_line.rstrip('\n') # chr(10), newline + physical_line = physical_line.rstrip('\r') # chr(13), carriage return + physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L + stripped = physical_line.rstrip(' \t\v') + if physical_line != stripped: + if stripped: + return len(stripped), "W291 trailing whitespace" + else: + return 0, "W293 blank line contains whitespace" + + +#def trailing_blank_lines(physical_line, lines, line_number): +# r""" +# JCR: Trailing blank lines are superfluous. +# +# Okay: spam(1) +# W391: spam(1)\n +# """ +# if not physical_line.rstrip() and line_number == len(lines): +# return 0, "W391 blank line at end of file" + + +def missing_newline(physical_line): + """ + JCR: The last line should have a newline. + + Reports warning W292. + """ + if physical_line.rstrip() == physical_line: + return len(physical_line), "W292 no newline at end of file" + + +def maximum_line_length(physical_line, max_line_length): + """ + Limit all lines to a maximum of 79 characters. + + There are still many devices around that are limited to 80 character + lines; plus, limiting windows to 80 characters makes it possible to have + several windows side-by-side. The default wrapping on such devices looks + ugly. Therefore, please limit all lines to a maximum of 79 characters. + For flowing long blocks of text (docstrings or comments), limiting the + length to 72 characters is recommended. + + Reports error E501. + """ + line = physical_line.rstrip() + length = len(line) + if length > max_line_length: + if hasattr(line, 'decode'): # Python 2 + # The line could contain multi-byte characters + try: + length = len(line.decode('utf-8')) + except UnicodeError: + pass + if length > max_line_length: + return (max_line_length, "E501 line too long " + "(%d > %d characters)" % (length, max_line_length)) + + +############################################################################## +# Plugins (check functions) for logical lines +############################################################################## + + +def blank_lines(logical_line, blank_lines, indent_level, line_number, + previous_logical, previous_indent_level): + r""" + Separate top-level function and class definitions with two blank lines. + + Method definitions inside a class are separated by a single blank line. + + Extra blank lines may be used (sparingly) to separate groups of related + functions. Blank lines may be omitted between a bunch of related + one-liners (e.g. a set of dummy implementations). + + Use blank lines in functions, sparingly, to indicate logical sections. + + Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass + + E301: class Foo:\n b = 0\n def bar():\n pass + E302: def a():\n pass\n\ndef b(n):\n pass + E303: def a():\n pass\n\n\n\ndef b(n):\n pass + E303: def a():\n\n\n\n pass + E304: @decorator\n\ndef a():\n pass + """ + if line_number == 1: + return # Don't expect blank lines before the first line + if previous_logical.startswith('@'): + if blank_lines: + yield 0, "E304 blank lines found after function decorator" + elif blank_lines > 2 or (indent_level and blank_lines == 2): + yield 0, "E303 too many blank lines (%d)" % blank_lines + elif logical_line.startswith(('def ', 'class ', '@')): + if indent_level: + if not (blank_lines or previous_indent_level < indent_level or + DOCSTRING_REGEX.match(previous_logical)): + yield 0, "E301 expected 1 blank line, found 0" + elif blank_lines != 2: + yield 0, "E302 expected 2 blank lines, found %d" % blank_lines + + +def extraneous_whitespace(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + + - Immediately before a comma, semicolon, or colon. + + Okay: spam(ham[1], {eggs: 2}) + E201: spam( ham[1], {eggs: 2}) + E201: spam(ham[ 1], {eggs: 2}) + E201: spam(ham[1], { eggs: 2}) + E202: spam(ham[1], {eggs: 2} ) + E202: spam(ham[1 ], {eggs: 2}) + E202: spam(ham[1], {eggs: 2 }) + + E203: if x == 4: print x, y; x, y = y , x + E203: if x == 4: print x, y ; x, y = y, x + E203: if x == 4 : print x, y; x, y = y, x + """ + line = logical_line + for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line): + text = match.group() + char = text.strip() + found = match.start() + if text == char + ' ': + # assert char in '([{' + yield found + 1, "E201 whitespace after '%s'" % char + elif line[found - 1] != ',': + code = ('E202' if char in '}])' else 'E203') # if char in ',;:' + yield found, "%s whitespace before '%s'" % (code, char) + + +def whitespace_around_keywords(logical_line): + r""" + Avoid extraneous whitespace around keywords. + + Okay: True and False + E271: True and False + E272: True and False + E273: True and\tFalse + E274: True\tand False + """ + for match in KEYWORD_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E274 tab before keyword" + elif len(before) > 1: + yield match.start(1), "E272 multiple spaces before keyword" + + if '\t' in after: + yield match.start(2), "E273 tab after keyword" + elif len(after) > 1: + yield match.start(2), "E271 multiple spaces after keyword" + + +def missing_whitespace(logical_line): + """ + JCR: Each comma, semicolon or colon should be followed by whitespace. + + Okay: [a, b] + Okay: (3,) + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] + E231: ['a','b'] + E231: foo(bar,baz) + """ + line = logical_line + for index in range(len(line) - 1): + char = line[index] + if char in ',;:' and line[index + 1] not in WHITESPACE: + before = line[:index] + if char == ':' and before.count('[') > before.count(']'): + continue # Slice syntax, no space required + if char == ',' and line[index + 1] == ')': + continue # Allow tuple with only one element: (3,) + yield index, "E231 missing whitespace after '%s'" % char + + +def indentation(logical_line, previous_logical, indent_char, + indent_level, previous_indent_level): + r""" + Use 4 spaces per indentation level. + + For really old code that you don't want to mess up, you can continue to + use 8-space tabs. + + Okay: a = 1 + Okay: if a == 0:\n a = 1 + E111: a = 1 + + Okay: for item in items:\n pass + E112: for item in items:\npass + + Okay: a = 1\nb = 2 + E113: a = 1\n b = 2 + """ + if indent_char == ' ' and indent_level % 4: + yield 0, "E111 indentation is not a multiple of four" + indent_expect = previous_logical.endswith(':') + if indent_expect and indent_level <= previous_indent_level: + yield 0, "E112 expected an indented block" + if indent_level > previous_indent_level and not indent_expect: + yield 0, "E113 unexpected indentation" + + +def continuation_line_indentation(logical_line, tokens, indent_level, verbose): + r""" + Continuation lines should align wrapped elements either vertically using + Python's implicit line joining inside parentheses, brackets and braces, or + using a hanging indent. + + When using a hanging indent the following considerations should be applied: + + - there should be no arguments on the first line, and + + - further indentation should be used to clearly distinguish itself as a + continuation line. + + Okay: a = (\n) + E123: a = (\n ) + + Okay: a = (\n 42) + E121: a = (\n 42) + E122: a = (\n42) + E123: a = (\n 42\n ) + E124: a = (24,\n 42\n) + E125: if (a or\n b):\n pass + E126: a = (\n 42) + E127: a = (24,\n 42) + E128: a = (24,\n 42) + """ + first_row = tokens[0][2][0] + nrows = 1 + tokens[-1][2][0] - first_row + if nrows == 1: + return + + # indent_next tells us whether the next block is indented; assuming + # that it is indented by 4 spaces, then we should not allow 4-space + # indents on the final continuation line; in turn, some other + # indents are allowed to have an extra 4 spaces. + indent_next = logical_line.endswith(':') + + row = depth = 0 + # remember how many brackets were opened on each line + parens = [0] * nrows + # relative indents of physical lines + rel_indent = [0] * nrows + # visual indents + indent = [indent_level] + indent_chances = {} + last_indent = (0, 0) + if verbose >= 3: + print((">>> " + tokens[0][4].rstrip())) + + for token_type, text, start, end, line in tokens: + newline = row < start[0] - first_row + if newline: + row = start[0] - first_row + newline = (not last_token_multiline and + token_type not in (tokenize.NL, tokenize.NEWLINE)) + + if newline: + # this is the beginning of a continuation line. + last_indent = start + if verbose >= 3: + print(("... " + line.rstrip())) + + # record the initial indent. + rel_indent[row] = start[1] - indent_level + + if depth: + # a bracket expression in a continuation line. + # find the line that it was opened on + for open_row in range(row - 1, -1, -1): + if parens[open_row]: + break + else: + # an unbracketed continuation line (ie, backslash) + open_row = 0 + hang = rel_indent[row] - rel_indent[open_row] + visual_indent = indent_chances.get(start[1]) + + if token_type == tokenize.OP and text in ']})': + # this line starts with a closing bracket + if indent[depth]: + if start[1] != indent[depth]: + yield (start, 'E124 closing bracket does not match ' + 'visual indentation') + elif hang: + yield (start, 'E123 closing bracket does not match ' + 'indentation of opening bracket\'s line') + elif visual_indent is True: + # visual indent is verified + if not indent[depth]: + indent[depth] = start[1] + elif visual_indent in (text, str): + # ignore token lined up with matching one from a previous line + pass + elif indent[depth] and start[1] < indent[depth]: + # visual indent is broken + yield (start, 'E128 continuation line ' + 'under-indented for visual indent') + elif hang == 4 or (indent_next and rel_indent[row] == 8): + # hanging indent is verified + pass + else: + # indent is broken + if hang <= 0: + error = 'E122', 'missing indentation or outdented' + elif indent[depth]: + error = 'E127', 'over-indented for visual indent' + elif hang % 4: + error = 'E121', 'indentation is not a multiple of four' + else: + error = 'E126', 'over-indented for hanging indent' + yield start, "%s continuation line %s" % error + + # look for visual indenting + if parens[row] and token_type != tokenize.NL and not indent[depth]: + indent[depth] = start[1] + indent_chances[start[1]] = True + if verbose >= 4: + print(("bracket depth %s indent to %s" % (depth, start[1]))) + # deal with implicit string concatenation + elif token_type == tokenize.STRING or text in ('u', 'ur', 'b', 'br'): + indent_chances[start[1]] = str + + # keep track of bracket depth + if token_type == tokenize.OP: + if text in '([{': + depth += 1 + indent.append(0) + parens[row] += 1 + if verbose >= 4: + print(("bracket depth %s seen, col %s, visual min = %s" % + (depth, start[1], indent[depth]))) + elif text in ')]}' and depth > 0: + # parent indents should not be more than this one + prev_indent = indent.pop() or last_indent[1] + for d in range(depth): + if indent[d] > prev_indent: + indent[d] = 0 + for ind in list(indent_chances): + if ind >= prev_indent: + del indent_chances[ind] + depth -= 1 + if depth: + indent_chances[indent[depth]] = True + for idx in range(row, -1, -1): + if parens[idx]: + parens[idx] -= 1 + break + assert len(indent) == depth + 1 + if start[1] not in indent_chances: + # allow to line up tokens + indent_chances[start[1]] = text + + last_token_multiline = (start[0] != end[0]) + + if indent_next and rel_indent[-1] == 4: + yield (last_indent, "E125 continuation line does not distinguish " + "itself from next logical line") + + +def whitespace_before_parameters(logical_line, tokens): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately before the open parenthesis that starts the argument + list of a function call. + + - Immediately before the open parenthesis that starts an indexing or + slicing. + + Okay: spam(1) + E211: spam (1) + + Okay: dict['key'] = list[index] + E211: dict ['key'] = list[index] + E211: dict['key'] = list [index] + """ + prev_type = tokens[0][0] + prev_text = tokens[0][1] + prev_end = tokens[0][3] + for index in range(1, len(tokens)): + token_type, text, start, end, line = tokens[index] + if (token_type == tokenize.OP and + text in '([' and + start != prev_end and + (prev_type == tokenize.NAME or prev_text in '}])') and + # Syntax "class A (B):" is allowed, but avoid it + (index < 2 or tokens[index - 2][1] != 'class') and + # Allow "return (a.foo for a in range(5))" + not keyword.iskeyword(prev_text)): + yield prev_end, "E211 whitespace before '%s'" % text + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_operator(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Okay: a = 12 + 3 + E221: a = 4 + 5 + E222: a = 4 + 5 + E223: a = 4\t+ 5 + E224: a = 4 +\t5 + """ + for match in OPERATOR_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E223 tab before operator" + elif len(before) > 1: + yield match.start(1), "E221 multiple spaces before operator" + + if '\t' in after: + yield match.start(2), "E224 tab after operator" + elif len(after) > 1: + yield match.start(2), "E222 multiple spaces after operator" + + +def missing_whitespace_around_operator(logical_line, tokens): + r""" + - Always surround these binary operators with a single space on + either side: assignment (=), augmented assignment (+=, -= etc.), + comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not), + Booleans (and, or, not). + + - Use spaces around arithmetic operators. + + Okay: i = i + 1 + Okay: submitted += 1 + Okay: x = x * 2 - 1 + Okay: hypot2 = x * x + y * y + Okay: c = (a + b) * (a - b) + Okay: foo(bar, key='word', *args, **kwargs) + Okay: baz(**kwargs) + Okay: negative = -1 + Okay: spam(-1) + Okay: alpha[:-i] + Okay: if not -5 < x < +5:\n pass + Okay: lambda *args, **kw: (args, kw) + + E225: i=i+1 + E225: submitted +=1 + E225: x = x*2 - 1 + E225: hypot2 = x*x + y*y + E225: c = (a+b) * (a-b) + E225: c = alpha -4 + E225: z = x **y + """ + parens = 0 + need_space = False + prev_type = tokenize.OP + prev_text = prev_end = None + for token_type, text, start, end, line in tokens: + if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN): + # ERRORTOKEN is triggered by backticks in Python 3000 + continue + if text in ('(', 'lambda'): + parens += 1 + elif text == ')': + parens -= 1 + if need_space: + if start != prev_end: + need_space = False + elif text == '>' and prev_text in ('<', '-'): + # Tolerate the "<>" operator, even if running Python 3 + # Deal with Python 3's annotated return value "->" + pass + else: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + elif token_type == tokenize.OP and prev_end is not None: + if text == '=' and parens: + # Allow keyword args or defaults: foo(bar=None). + pass + elif text in BINARY_OPERATORS: + need_space = True + elif text in UNARY_OPERATORS: + # Allow unary operators: -123, -x, +1. + # Allow argument unpacking: foo(*args, **kwargs). + if prev_type == tokenize.OP: + if prev_text in '}])': + need_space = True + elif prev_type == tokenize.NAME: + if prev_text not in KEYWORDS: + need_space = True + elif prev_type not in SKIP_TOKENS: + need_space = True + if need_space and start == prev_end: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_comma(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Note: these checks are disabled by default + + Okay: a = (1, 2) + E241: a = (1, 2) + E242: a = (1,\t2) + """ + line = logical_line + for m in WHITESPACE_AFTER_COMMA_REGEX.finditer(line): + found = m.start() + 1 + if '\t' in m.group(): + yield found, "E242 tab after '%s'" % m.group()[0] + else: + yield found, "E241 multiple spaces after '%s'" % m.group()[0] + + +def whitespace_around_named_parameter_equals(logical_line, tokens): + """ + Don't use spaces around the '=' sign when used to indicate a + keyword argument or a default parameter value. + + Okay: def complex(real, imag=0.0): + Okay: return magic(r=real, i=imag) + Okay: boolean(a == b) + Okay: boolean(a != b) + Okay: boolean(a <= b) + Okay: boolean(a >= b) + + E251: def complex(real, imag = 0.0): + E251: return magic(r = real, i = imag) + """ + parens = 0 + no_space = False + prev_end = None + for token_type, text, start, end, line in tokens: + if no_space: + no_space = False + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + elif token_type == tokenize.OP: + if text == '(': + parens += 1 + elif text == ')': + parens -= 1 + elif parens and text == '=': + no_space = True + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + prev_end = end + + +def whitespace_before_inline_comment(logical_line, tokens): + """ + Separate inline comments by at least two spaces. + + An inline comment is a comment on the same line as a statement. Inline + comments should be separated by at least two spaces from the statement. + They should start with a # and a single space. + + Okay: x = x + 1 # Increment x + Okay: x = x + 1 # Increment x + E261: x = x + 1 # Increment x + E262: x = x + 1 #Increment x + E262: x = x + 1 # Increment x + """ + prev_end = (0, 0) + for token_type, text, start, end, line in tokens: + if token_type == tokenize.COMMENT: + if not line[:start[1]].strip(): + continue + if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: + yield (prev_end, + "E261 at least two spaces before inline comment") + if text.startswith('# ') or not text.startswith('# '): + yield start, "E262 inline comment should start with '# '" + elif token_type != tokenize.NL: + prev_end = end + + +def imports_on_separate_lines(logical_line): + r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + Okay: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + line = logical_line + if line.startswith('import '): + found = line.find(',') + if -1 < found: + yield found, "E401 multiple imports on one line" + + +def compound_statements(logical_line): + r""" + Compound statements (multiple statements on the same line) are + generally discouraged. + + While sometimes it's okay to put an if/for/while with a small body + on the same line, never do this for multi-clause statements. Also + avoid folding such long lines! + + Okay: if foo == 'blah':\n do_blah_thing() + Okay: do_one() + Okay: do_two() + Okay: do_three() + + E701: if foo == 'blah': do_blah_thing() + E701: for x in lst: total += x + E701: while t < 10: t = delay() + E701: if foo == 'blah': do_blah_thing() + E701: else: do_non_blah_thing() + E701: try: something() + E701: finally: cleanup() + E701: if foo == 'blah': one(); two(); three() + + E702: do_one(); do_two(); do_three() + """ + line = logical_line + found = line.find(':') + if -1 < found < len(line) - 1: + before = line[:found] + if (before.count('{') <= before.count('}') and # {'a': 1} (dict) + before.count('[') <= before.count(']') and # [1:2] (slice) + before.count('(') <= before.count(')') and # (Python 3 annotation) + not LAMBDA_REGEX.search(before)): # lambda x: x + yield found, "E701 multiple statements on one line (colon)" + found = line.find(';') + if -1 < found: + yield found, "E702 multiple statements on one line (semicolon)" + + +def explicit_line_join(logical_line, tokens): + r""" + Avoid explicit line join between brackets. + + The preferred way of wrapping long lines is by using Python's implied line + continuation inside parentheses, brackets and braces. Long lines can be + broken over multiple lines by wrapping expressions in parentheses. These + should be used in preference to using a backslash for line continuation. + + E502: aaa = [123, \\n 123] + E502: aaa = ("bbb " \\n "ccc") + + Okay: aaa = [123,\n 123] + Okay: aaa = ("bbb "\n "ccc") + Okay: aaa = "bbb " \\n "ccc" + """ + prev_start = prev_end = parens = 0 + for token_type, text, start, end, line in tokens: + if start[0] != prev_start and parens and backslash: + yield backslash, "E502 the backslash is redundant between brackets" + if end[0] != prev_end: + if line.rstrip('\r\n').endswith('\\'): + backslash = (end[0], len(line.splitlines()[-1]) - 1) + else: + backslash = None + prev_start = prev_end = end[0] + else: + prev_start = start[0] + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in ')]}': + parens -= 1 + + +def comparison_to_singleton(logical_line): + """ + Comparisons to singletons like None should always be done + with "is" or "is not", never the equality operators. + + Okay: if arg is not None: + E711: if arg != None: + E712: if arg == True: + + Also, beware of writing if x when you really mean if x is not None -- + e.g. when testing whether a variable or argument that defaults to None was + set to some other value. The other value might have a type (such as a + container) that could be false in a boolean context! + """ + match = COMPARE_SINGLETON_REGEX.search(logical_line) + if match: + same = (match.group(1) == '==') + singleton = match.group(2) + msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton) + if singleton in ('None',): + code = 'E711' + else: + code = 'E712' + nonzero = ((singleton == 'True' and same) or + (singleton == 'False' and not same)) + msg += " or 'if %scond:'" % ('' if nonzero else 'not ') + yield match.start(1), ("%s comparison to %s should be %s" % + (code, singleton, msg)) + + +def comparison_type(logical_line): + """ + Object type comparisons should always use isinstance() instead of + comparing types directly. + + Okay: if isinstance(obj, int): + E721: if type(obj) is type(1): + + When checking if an object is a string, keep in mind that it might be a + unicode string too! In Python 2.3, str and unicode have a common base + class, basestring, so you can do: + + Okay: if isinstance(obj, basestring): + Okay: if type(a1) is type(b1): + """ + match = COMPARE_TYPE_REGEX.search(logical_line) + if match: + inst = match.group(3) + if inst and isidentifier(inst) and inst not in SINGLETONS: + return # Allow comparison for types which are not obvious + yield match.start(1), "E721 do not compare types, use 'isinstance()'" + + +def python_3000_has_key(logical_line): + r""" + The {}.has_key() method will be removed in the future version of + Python. Use the 'in' operation instead. + + Okay: if "alph" in d:\n print d["alph"] + W601: assert d.has_key('alph') + """ + pos = logical_line.find('.has_key(') + if pos > -1: + yield pos, "W601 .has_key() is deprecated, use 'in'" + + +def python_3000_raise_comma(logical_line): + """ + When raising an exception, use "raise ValueError('message')" + instead of the older form "raise ValueError, 'message'". + + The paren-using form is preferred because when the exception arguments + are long or include string formatting, you don't need to use line + continuation characters thanks to the containing parentheses. The older + form will be removed in Python 3000. + + Okay: raise DummyError("Message") + W602: raise DummyError, "Message" + """ + match = RAISE_COMMA_REGEX.match(logical_line) + if match and not RERAISE_COMMA_REGEX.match(logical_line): + yield match.start(1), "W602 deprecated form of raising exception" + + +def python_3000_not_equal(logical_line): + """ + != can also be written <>, but this is an obsolete usage kept for + backwards compatibility only. New code should always use !=. + The older syntax is removed in Python 3000. + + Okay: if a != 'no': + W603: if a <> 'no': + """ + pos = logical_line.find('<>') + if pos > -1: + yield pos, "W603 '<>' is deprecated, use '!='" + + +def python_3000_backticks(logical_line): + """ + Backticks are removed in Python 3000. + Use repr() instead. + + Okay: val = repr(1 + 2) + W604: val = `1 + 2` + """ + pos = logical_line.find('`') + if pos > -1: + yield pos, "W604 backticks are deprecated, use 'repr()'" + + +############################################################################## +# Helper functions +############################################################################## + + +if '' == ''.encode(): + # Python 2: implicit encoding. + def readlines(filename): + f = open(filename) + try: + return f.readlines() + finally: + f.close() + + isidentifier = re.compile(r'[a-zA-Z_]\w*').match + stdin_get_value = sys.stdin.read +else: + # Python 3 + def readlines(filename): + f = open(filename, 'rb') + try: + coding, lines = tokenize.detect_encoding(f.readline) + f = TextIOWrapper(f, coding, line_buffering=True) + return [l.decode(coding) for l in lines] + f.readlines() + except (LookupError, SyntaxError, UnicodeError): + f.close() + # Fall back if files are improperly declared + f = open(filename, encoding='latin-1') + return f.readlines() + finally: + f.close() + + isidentifier = str.isidentifier + stdin_get_value = TextIOWrapper(sys.stdin.buffer, errors='ignore').read +readlines.__doc__ = " Read the source code." + + +def expand_indent(line): + r""" + Return the amount of indentation. + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 16 + """ + if '\t' not in line: + return len(line) - len(line.lstrip()) + result = 0 + for char in line: + if char == '\t': + result = result // 8 * 8 + 8 + elif char == ' ': + result += 1 + else: + break + return result + + +def mute_string(text): + """ + Replace contents with 'xxx' to prevent syntax matching. + + >>> mute_string('"abc"') + '"xxx"' + >>> mute_string("'''abc'''") + "'''xxx'''" + >>> mute_string("r'abc'") + "r'xxx'" + """ + # String modifiers (e.g. u or r) + start = text.index(text[-1]) + 1 + end = len(text) - 1 + # Triple quotes + if text[-3:] in ('"""', "'''"): + start += 2 + end -= 2 + return text[:start] + 'x' * (end - start) + text[end:] + + +def parse_udiff(diff, patterns=None, parent='.'): + rv = {} + path = nrows = None + for line in diff.splitlines(): + if nrows: + if line[:1] != '-': + nrows -= 1 + continue + if line[:3] == '@@ ': + row, nrows = [int(g) for g in HUNK_REGEX.match(line).groups()] + rv[path].update(list(range(row, row + nrows))) + elif line[:3] == '+++': + path = line[4:].split('\t', 1)[0] + if path[:2] == 'b/': + path = path[2:] + rv[path] = set() + return dict([(os.path.join(parent, path), rows) + for (path, rows) in list(rv.items()) + if rows and filename_match(path, patterns)]) + + +def filename_match(filename, patterns, default=True): + """ + Check if patterns contains a pattern that matches filename. + If patterns is unspecified, this always returns True. + """ + if not patterns: + return default + return any(fnmatch(filename, pattern) for pattern in patterns) + + +############################################################################## +# Framework to run all checks +############################################################################## + + +def find_checks(argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name. + """ + for name, function in list(globals().items()): + if not inspect.isfunction(function): + continue + args = inspect.getargspec(function)[0] + if args and args[0].startswith(argument_name): + codes = ERRORCODE_REGEX.findall(function.__doc__ or '') + yield name, codes, function, args + + +class Checker(object): + """ + Load a Python source file, tokenize it, check coding style. + """ + + def __init__(self, filename, lines=None, + options=None, report=None, **kwargs): + if options is None: + options = StyleGuide(kwargs).options + else: + assert not kwargs + self._io_error = None + self._physical_checks = options.physical_checks + self._logical_checks = options.logical_checks + self.max_line_length = options.max_line_length + self.verbose = options.verbose + self.filename = filename + if filename is None: + self.filename = 'stdin' + self.lines = lines or [] + elif lines is None: + try: + self.lines = readlines(filename) + except IOError: + exc_type, exc = sys.exc_info()[:2] + self._io_error = '%s: %s' % (exc_type.__name__, exc) + self.lines = [] + else: + self.lines = lines + self.report = report or options.report + self.report_error = self.report.error + + def readline(self): + """ + Get the next line from the input buffer. + """ + self.line_number += 1 + if self.line_number > len(self.lines): + return '' + return self.lines[self.line_number - 1] + + def readline_check_physical(self): + """ + Check and return the next physical line. This method can be + used to feed tokenize.generate_tokens. + """ + line = self.readline() + if line: + self.check_physical(line) + return line + + def run_check(self, check, argument_names): + """ + Run a check plugin. + """ + arguments = [] + for name in argument_names: + arguments.append(getattr(self, name)) + return check(*arguments) + + def check_physical(self, line): + """ + Run all physical checks on a raw input line. + """ + self.physical_line = line + if self.indent_char is None and line[:1] in WHITESPACE: + self.indent_char = line[0] + for name, check, argument_names in self._physical_checks: + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + self.report_error(self.line_number, offset, text, check) + + def build_tokens_line(self): + """ + Build a logical line from tokens. + """ + self.mapping = [] + logical = [] + length = 0 + previous = None + for token in self.tokens: + token_type, text = token[0:2] + if token_type in SKIP_TOKENS: + continue + if token_type == tokenize.STRING: + text = mute_string(text) + if previous: + end_row, end = previous[3] + start_row, start = token[2] + if end_row != start_row: # different row + prev_text = self.lines[end_row - 1][end - 1] + if prev_text == ',' or (prev_text not in '{[(' + and text not in '}])'): + logical.append(' ') + length += 1 + elif end != start: # different column + fill = self.lines[end_row - 1][end:start] + logical.append(fill) + length += len(fill) + self.mapping.append((length, token)) + logical.append(text) + length += len(text) + previous = token + self.logical_line = ''.join(logical) + assert self.logical_line.strip() == self.logical_line + + def check_logical(self): + """ + Build a line from tokens and run all logical checks on it. + """ + self.build_tokens_line() + self.report.increment_logical_line() + first_line = self.lines[self.mapping[0][1][2][0] - 1] + indent = first_line[:self.mapping[0][1][2][1]] + self.previous_indent_level = self.indent_level + self.indent_level = expand_indent(indent) + if self.verbose >= 2: + print((self.logical_line[:80].rstrip())) + for name, check, argument_names in self._logical_checks: + if self.verbose >= 4: + print((' ' + name)) + for result in self.run_check(check, argument_names): + offset, text = result + if isinstance(offset, tuple): + orig_number, orig_offset = offset + else: + for token_offset, token in self.mapping: + if offset >= token_offset: + orig_number = token[2][0] + orig_offset = (token[2][1] + offset - token_offset) + self.report_error(orig_number, orig_offset, text, check) + self.previous_logical = self.logical_line + + def generate_tokens(self): + if self._io_error: + self.report_error(1, 0, 'E902 %s' % self._io_error, readlines) + tokengen = tokenize.generate_tokens(self.readline_check_physical) + try: + for token in tokengen: + yield token + except (SyntaxError, tokenize.TokenError): + exc_type, exc = sys.exc_info()[:2] + offset = exc.args[1] + if len(offset) > 2: + offset = offset[1:3] + self.report_error(offset[0], offset[1], + 'E901 %s: %s' % (exc_type.__name__, exc.args[0]), + self.generate_tokens) + generate_tokens.__doc__ = " Check if the syntax is valid." + + def check_all(self, expected=None, line_offset=0): + """ + Run all checks on the input file. + """ + self.report.init_file(self.filename, self.lines, expected, line_offset) + self.line_number = 0 + self.indent_char = None + self.indent_level = 0 + self.previous_logical = '' + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + parens = 0 + for token in self.generate_tokens(): + self.tokens.append(token) + token_type, text = token[0:2] + if self.verbose >= 3: + if token[2][0] == token[3][0]: + pos = '[%s:%s]' % (token[2][1] or '', token[3][1]) + else: + pos = 'l.%s' % token[3][0] + print(('l.%s\t%s\t%s\t%r' % + (token[2][0], pos, tokenize.tok_name[token[0]], text))) + if token_type == tokenize.COMMENT or token_type == tokenize.STRING: + for sre in re.finditer(r"[:.;,] ?[A-Za-z]", text): + pos = sre.span()[0] + part = text[:pos] + line = token[2][0] + part.count('\n') + offset = 0 if part.count('\n') > 0 else token[2][1] + col = offset + pos - part.rfind('\n') + 1 + if sre.group(0)[0] == '.': + self.report_error(line, col, + 'E289 Too many spaces after period. Use only one.', + check=None) + elif sre.group(0)[0] == ',': + self.report_error(line, col, + 'E288 Too many spaces after comma. Use only one.', + check=None) + else: + self.report_error(line, col, + 'E287 Too many spaces after punctuation. ' + 'Use only one.', + check=None) + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in '}])': + parens -= 1 + elif not parens: + if token_type == tokenize.NEWLINE: + if self.blank_lines < blank_lines_before_comment: + self.blank_lines = blank_lines_before_comment + self.check_logical() + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + elif token_type == tokenize.NL: + if len(self.tokens) == 1: + # The physical line contains only this token. + self.blank_lines += 1 + self.tokens = [] + elif token_type == tokenize.COMMENT and len(self.tokens) == 1: + if blank_lines_before_comment < self.blank_lines: + blank_lines_before_comment = self.blank_lines + self.blank_lines = 0 + if COMMENT_WITH_NL: + # The comment also ends a physical line + self.tokens = [] + if self.blank_lines > 1: + self.report_error(token[2][0],0, + 'E389 File ends in multiple blank lines', + check=None) + + return self.report.get_file_results() + + +class BaseReport(object): + """Collect the results of the checks.""" + print_filename = False + + def __init__(self, options): + self._benchmark_keys = options.benchmark_keys + self._ignore_code = options.ignore_code + # Results + self.elapsed = 0 + self.total_errors = 0 + self.counters = dict.fromkeys(self._benchmark_keys, 0) + self.messages = {} + + def start(self): + """Start the timer.""" + self._start_time = time.time() + + def stop(self): + """Stop the timer.""" + self.elapsed = time.time() - self._start_time + + def init_file(self, filename, lines, expected, line_offset): + """Signal a new file.""" + self.filename = filename + self.lines = lines + self.expected = expected or () + self.line_offset = line_offset + self.file_errors = 0 + self.counters['files'] += 1 + self.counters['physical lines'] += len(lines) + + def increment_logical_line(self): + """Signal a new logical line.""" + self.counters['logical lines'] += 1 + + def error(self, line_number, offset, text, check): + """Report an error, according to options.""" + code = text[:4] + if self._ignore_code(code): + return + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + self.messages[code] = text[5:] + # Don't care about expected errors or warnings + if code in self.expected: + return + if self.print_filename and not self.file_errors: + print((self.filename)) + self.file_errors += 1 + self.total_errors += 1 + return code + + def get_file_results(self): + """Return the count of errors and warnings for this file.""" + return self.file_errors + + def get_count(self, prefix=''): + """Return the total count of errors and warnings.""" + return sum([self.counters[key] + for key in self.messages if key.startswith(prefix)]) + + def get_statistics(self, prefix=''): + """ + Get statistics for message codes that start with the prefix. + + prefix='' matches all errors and warnings + prefix='E' matches all errors + prefix='W' matches all warnings + prefix='E4' matches all errors that have to do with imports + """ + return ['%-7s %s %s' % (self.counters[key], key, self.messages[key]) + for key in sorted(self.messages) if key.startswith(prefix)] + + def print_statistics(self, prefix=''): + """Print overall statistics (number of errors and warnings).""" + for line in self.get_statistics(prefix): + print(line) + + def print_benchmark(self): + """Print benchmark numbers.""" + print(('%-7.2f %s' % (self.elapsed, 'seconds elapsed'))) + if self.elapsed: + for key in self._benchmark_keys: + print(('%-7d %s per second (%d total)' % + (self.counters[key] / self.elapsed, key, + self.counters[key]))) + + +class FileReport(BaseReport): + print_filename = True + + +class StandardReport(BaseReport): + """Collect and print the results of the checks.""" + + def __init__(self, options): + super(StandardReport, self).__init__(options) + self._fmt = REPORT_FORMAT.get(options.format.lower(), + options.format) + self._repeat = options.repeat + self._show_source = options.show_source + self._show_pep8 = options.show_pep8 + + def error(self, line_number, offset, text, check): + """ + Report an error, according to options. + """ + code = super(StandardReport, self).error(line_number, offset, + text, check) + if code and (self.counters[code] == 1 or self._repeat): + print((self._fmt % { + 'path': self.filename, + 'row': self.line_offset + line_number, 'col': offset + 1, + 'code': code, 'text': text[5:], + })) + if self._show_source: + if line_number > len(self.lines): + line = '' + else: + line = self.lines[line_number - 1] + print((line.rstrip())) + print((' ' * offset + '^')) + if self._show_pep8 and check is not None: + print((check.__doc__.lstrip('\n').rstrip())) + return code + + +class DiffReport(StandardReport): + """Collect and print the results for the changed lines only.""" + + def __init__(self, options): + super(DiffReport, self).__init__(options) + self._selected = options.selected_lines + + def error(self, line_number, offset, text, check): + if line_number not in self._selected[self.filename]: + return + return super(DiffReport, self).error(line_number, offset, text, check) + + +class TestReport(StandardReport): + """Collect the results for the tests.""" + + def __init__(self, options): + options.benchmark_keys += ['test cases', 'failed tests'] + super(TestReport, self).__init__(options) + self._verbose = options.verbose + + def get_file_results(self): + # Check if the expected errors were found + label = '%s:%s:1' % (self.filename, self.line_offset) + codes = sorted(self.expected) + for code in codes: + if not self.counters.get(code): + self.file_errors += 1 + self.total_errors += 1 + print(('%s: error %s not found' % (label, code))) + if self._verbose and not self.file_errors: + print(('%s: passed (%s)' % + (label, ' '.join(codes) or 'Okay'))) + self.counters['test cases'] += 1 + if self.file_errors: + self.counters['failed tests'] += 1 + # Reset counters + for key in set(self.counters) - set(self._benchmark_keys): + del self.counters[key] + self.messages = {} + return self.file_errors + + def print_results(self): + results = ("%(physical lines)d lines tested: %(files)d files, " + "%(test cases)d test cases%%s." % self.counters) + if self.total_errors: + print((results % ", %s failures" % self.total_errors)) + else: + print((results % "")) + print(("Test failed." if self.total_errors else "Test passed.")) + + +class StyleGuide(object): + """Initialize a PEP-8 instance with few options.""" + + def __init__(self, *args, **kwargs): + # build options from the command line + parse_argv = kwargs.pop('parse_argv', False) + config_file = kwargs.pop('config_file', None) + options, self.paths = process_options(parse_argv=parse_argv, + config_file=config_file) + if args or kwargs: + # build options from dict + options_dict = dict(*args, **kwargs) + options.__dict__.update(options_dict) + if 'paths' in options_dict: + self.paths = options_dict['paths'] + + self.runner = self.input_file + self.options = options + + if not options.reporter: + options.reporter = BaseReport if options.quiet else StandardReport + + for index, value in enumerate(options.exclude): + options.exclude[index] = value.rstrip('/') + # Ignore all checks which are not explicitly selected + options.select = tuple(options.select or ()) + options.ignore = tuple(options.ignore or options.select and ('',)) + options.benchmark_keys = BENCHMARK_KEYS[:] + options.ignore_code = self.ignore_code + options.physical_checks = self.get_checks('physical_line') + options.logical_checks = self.get_checks('logical_line') + self.init_report() + + def init_report(self, reporter=None): + """Initialize the report instance.""" + self.options.report = (reporter or self.options.reporter)(self.options) + return self.options.report + + def check_files(self, paths=None): + """Run all checks on the paths.""" + if paths is None: + paths = self.paths + report = self.options.report + runner = self.runner + report.start() + for path in paths: + if os.path.isdir(path): + self.input_dir(path) + elif not self.excluded(path): + runner(path) + report.stop() + return report + + def input_file(self, filename, lines=None, expected=None, line_offset=0): + """Run all checks on a Python source file.""" + if self.options.verbose: + print(('checking %s' % filename)) + fchecker = Checker(filename, lines=lines, options=self.options) + return fchecker.check_all(expected=expected, line_offset=line_offset) + + def input_dir(self, dirname): + """Check all files in this directory and all subdirectories.""" + dirname = dirname.rstrip('/') + if self.excluded(dirname): + return 0 + counters = self.options.report.counters + verbose = self.options.verbose + filepatterns = self.options.filename + runner = self.runner + for root, dirs, files in os.walk(dirname): + if verbose: + print(('directory ' + root)) + counters['directories'] += 1 + for subdir in sorted(dirs): + if self.excluded(subdir): + dirs.remove(subdir) + for filename in sorted(files): + # contain a pattern that matches? + if ((filename_match(filename, filepatterns) and + not self.excluded(filename))): + runner(os.path.join(root, filename)) + + def excluded(self, filename): + """ + Check if options.exclude contains a pattern that matches filename. + """ + basename = os.path.basename(filename) + return filename_match(basename, self.options.exclude, default=False) + + def ignore_code(self, code): + """ + Check if the error code should be ignored. + + If 'options.select' contains a prefix of the error code, + return False. Else, if 'options.ignore' contains a prefix of + the error code, return True. + """ + return (code.startswith(self.options.ignore) and + not code.startswith(self.options.select)) + + def get_checks(self, argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name and which contain selected tests. + """ + checks = [] + for name, codes, function, args in find_checks(argument_name): + if any(not (code and self.ignore_code(code)) for code in codes): + checks.append((name, function, args)) + return sorted(checks) + + +def init_tests(pep8style): + """ + Initialize testing framework. + + A test file can provide many tests. Each test starts with a + declaration. This declaration is a single line starting with '#:'. + It declares codes of expected failures, separated by spaces or 'Okay' + if no failure is expected. + If the file does not contain such declaration, it should pass all + tests. If the declaration is empty, following lines are not checked, + until next declaration. + + Examples: + + * Only E224 and W701 are expected: #: E224 W701 + * Following example is conform: #: Okay + * Don't check these lines: #: + """ + report = pep8style.init_report(TestReport) + runner = pep8style.input_file + + def run_tests(filename): + """Run all the tests from a file.""" + lines = readlines(filename) + ['#:\n'] + line_offset = 0 + codes = ['Okay'] + testcase = [] + count_files = report.counters['files'] + for index, line in enumerate(lines): + if not line.startswith('#:'): + if codes: + # Collect the lines of the test case + testcase.append(line) + continue + if codes and index: + codes = [c for c in codes if c != 'Okay'] + # Run the checker + runner(filename, testcase, expected=codes, + line_offset=line_offset) + # output the real line numbers + line_offset = index + 1 + # configure the expected errors + codes = line.split()[1:] + # empty the test case buffer + del testcase[:] + report.counters['files'] = count_files + 1 + return report.counters['failed tests'] + + pep8style.runner = run_tests + + +def selftest(options): + """ + Test all check functions with test cases in docstrings. + """ + count_failed = count_all = 0 + report = BaseReport(options) + counters = report.counters + checks = options.physical_checks + options.logical_checks + for name, check, argument_names in checks: + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match is None: + continue + code, source = match.groups() + checker = Checker(None, options=options, report=report) + for part in source.split(r'\n'): + part = part.replace(r'\t', '\t') + part = part.replace(r'\s', ' ') + checker.lines.append(part + '\n') + checker.check_all() + error = None + if code == 'Okay': + if len(counters) > len(options.benchmark_keys): + codes = [key for key in counters + if key not in options.benchmark_keys] + error = "incorrectly found %s" % ', '.join(codes) + elif not counters.get(code): + error = "failed to find %s" % code + # Keep showing errors for multiple tests + for key in set(counters) - set(options.benchmark_keys): + del counters[key] + report.messages = {} + count_all += 1 + if not error: + if options.verbose: + print(("%s: %s" % (code, source))) + else: + count_failed += 1 + print(("%s: %s:" % (__file__, error))) + for line in checker.lines: + print((line.rstrip())) + return count_failed, count_all + + +def read_config(options, args, arglist, parser): + """Read both user configuration and local configuration.""" + config = RawConfigParser() + + user_conf = options.config + if user_conf and os.path.isfile(user_conf): + if options.verbose: + print(('user configuration: %s' % user_conf)) + config.read(user_conf) + + parent = tail = args and os.path.abspath(os.path.commonprefix(args)) + while tail: + local_conf = os.path.join(parent, '.pep8') + if os.path.isfile(local_conf): + if options.verbose: + print(('local configuration: %s' % local_conf)) + config.read(local_conf) + break + parent, tail = os.path.split(parent) + + if config.has_section('pep8'): + option_list = dict([(o.dest, o.type or o.action) + for o in parser.option_list]) + + # First, read the default values + new_options, _ = parser.parse_args([]) + + # Second, parse the configuration + for opt in config.options('pep8'): + if options.verbose > 1: + print((' %s = %s' % (opt, config.get('pep8', opt)))) + if opt.replace('_', '-') not in parser.config_options: + print(('Unknown option: \'%s\'\n not in [%s]' % + (opt, ' '.join(parser.config_options)))) + sys.exit(1) + normalized_opt = opt.replace('-', '_') + opt_type = option_list[normalized_opt] + if opt_type in ('int', 'count'): + value = config.getint('pep8', opt) + elif opt_type == 'string': + value = config.get('pep8', opt) + else: + assert opt_type in ('store_true', 'store_false') + value = config.getboolean('pep8', opt) + setattr(new_options, normalized_opt, value) + + # Third, overwrite with the command-line options + options, _ = parser.parse_args(arglist, values=new_options) + + return options + + +def process_options(arglist=None, parse_argv=False, config_file=None): + """Process options passed either via arglist or via command line args.""" + if not arglist and not parse_argv: + # Don't read the command line if the module is used as a library. + arglist = [] + if config_file is True: + config_file = DEFAULT_CONFIG + parser = OptionParser(version=__version__, + usage="%prog [options] input ...") + parser.config_options = [ + 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count', + 'format', 'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose'] + parser.add_option('-v', '--verbose', default=0, action='count', + help="print status messages, or debug with -vv") + parser.add_option('-q', '--quiet', default=0, action='count', + help="report only file names, or nothing with -qq") + parser.add_option('-r', '--repeat', default=True, action='store_true', + help="(obsolete) show all occurrences of the same error") + parser.add_option('--first', action='store_false', dest='repeat', + help="show first occurrence of each error") + parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, + help="exclude files or directories which match these " + "comma separated patterns (default: %default)") + parser.add_option('--filename', metavar='patterns', default='*.py', + help="when parsing directories, only check filenames " + "matching these comma separated patterns " + "(default: %default)") + parser.add_option('--select', metavar='errors', default='', + help="select errors and warnings (e.g. E,W6)") + parser.add_option('--ignore', metavar='errors', default='', + help="skip errors and warnings (e.g. E4,W)") + parser.add_option('--show-source', action='store_true', + help="show source code for each error") + parser.add_option('--show-pep8', action='store_true', + help="show text of PEP 8 for each error " + "(implies --first)") + parser.add_option('--statistics', action='store_true', + help="count errors and warnings") + parser.add_option('--count', action='store_true', + help="print total number of errors and warnings " + "to standard error and set exit code to 1 if " + "total is not null") + parser.add_option('--max-line-length', type='int', metavar='n', + default=MAX_LINE_LENGTH, + help="set maximum allowed line length " + "(default: %default)") + parser.add_option('--format', metavar='format', default='default', + help="set the error format [default|pylint|<custom>]") + parser.add_option('--diff', action='store_true', + help="report only lines changed according to the " + "unified diff received on STDIN") + group = parser.add_option_group("Testing Options") + group.add_option('--testsuite', metavar='dir', + help="run regression tests from dir") + group.add_option('--doctest', action='store_true', + help="run doctest on myself") + group.add_option('--benchmark', action='store_true', + help="measure processing speed") + group = parser.add_option_group("Configuration", description=( + "The project options are read from the [pep8] section of the .pep8 " + "file located in any parent folder of the path(s) being processed. " + "Allowed options are: %s." % ', '.join(parser.config_options))) + group.add_option('--config', metavar='path', default=config_file, + help="config file location (default: %default)") + + options, args = parser.parse_args(arglist) + options.reporter = None + + if options.testsuite: + args.append(options.testsuite) + elif not options.doctest: + if parse_argv and not args: + if os.path.exists('.pep8') or options.diff: + args = ['.'] + else: + parser.error('input not specified') + options = read_config(options, args, arglist, parser) + options.reporter = parse_argv and options.quiet == 1 and FileReport + + if options.filename: + options.filename = options.filename.split(',') + options.exclude = options.exclude.split(',') + if options.select: + options.select = options.select.split(',') + if options.ignore: + options.ignore = options.ignore.split(',') + elif not (options.select or + options.testsuite or options.doctest) and DEFAULT_IGNORE: + # The default choice: ignore controversial checks + # (for doctest and testsuite, all checks are required) + options.ignore = DEFAULT_IGNORE.split(',') + + if options.diff: + options.reporter = DiffReport + stdin = stdin_get_value() + options.selected_lines = parse_udiff(stdin, options.filename, args[0]) + args = sorted(options.selected_lines) + + return options, args + + +def _main(): + """Parse options and run checks on Python source.""" + pep8style = StyleGuide(parse_argv=True, config_file=True) + options = pep8style.options + if options.doctest: + import doctest + fail_d, done_d = doctest.testmod(report=False, verbose=options.verbose) + fail_s, done_s = selftest(options) + count_failed = fail_s + fail_d + if not options.quiet: + count_passed = done_d + done_s - count_failed + print(("%d passed and %d failed." % (count_passed, count_failed))) + print(("Test failed." if count_failed else "Test passed.")) + if count_failed: + sys.exit(1) + if options.testsuite: + init_tests(pep8style) + report = pep8style.check_files() + if options.statistics: + report.print_statistics() + if options.benchmark: + report.print_benchmark() + if options.testsuite and not options.quiet: + report.print_results() + if report.total_errors: + if options.count: + sys.stderr.write(str(report.total_errors) + '\n') + sys.exit(1) + + +if __name__ == '__main__': + _main() diff --git a/external/plyer/tools/pep8checker/pep8base.html b/external/plyer/tools/pep8checker/pep8base.html new file mode 100644 index 0000000..e69ca6f --- /dev/null +++ b/external/plyer/tools/pep8checker/pep8base.html @@ -0,0 +1,70 @@ +<html> + <head> + <title>Kivy Styleguide Check</title> + <style type="text/css"> + body, html + { + background: black url('data:image/png;base64,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') repeat; color: #2D3841; + font-weight: 100; + margin: 0; + } + + table + { + width: 100%%; + color: #BDC8D1; + } + + table th, + table td + { + padding: 8px; + } + + table tr:hover td + { + background-color: #393939; + } + + table th + { + background-color: #191919; + text-align: left !important; + } + + #header + { + clear: both; + padding: 0px 0px 10px 20px; + height: 150px; + } + + #wrapper + { + margin: auto; + width: 800px; + color: #BDC8D1; + } + + #wrapper h1 + { + background-color: #191919; + color: white; + letter-spacing: 2px; + margin: 0px; + padding: 8px 15px; + font-family: 'Lucida Grande',Calibri,Verdana,sans-serif; + font-weight: 100; + } + + .page-content { + padding: 20px; + background-color:#232323; + } + </style> + </head> + <body> + <div id="wrapper"> + <h1>Kivy Styleguide (PEP8) Check</h1> + <div class="page-content"> + diff --git a/external/plyer/tools/pep8checker/pep8kivy.py b/external/plyer/tools/pep8checker/pep8kivy.py new file mode 100644 index 0000000..245928e --- /dev/null +++ b/external/plyer/tools/pep8checker/pep8kivy.py @@ -0,0 +1,109 @@ +import sys +from os import walk +from os.path import isdir, join, abspath, dirname +import pep8 +import time + +htmlmode = False + +pep8_ignores = ( + 'E125', # continuation line does not + # distinguish itself from next logical line + 'E126', # continuation line over-indented for hanging indent + 'E127', # continuation line over-indented for visual indent + 'E128') # continuation line under-indented for visual indent + +class KivyStyleChecker(pep8.Checker): + + def __init__(self, filename): + pep8.Checker.__init__(self, filename, ignore=pep8_ignores) + + def report_error(self, line_number, offset, text, check): + if htmlmode is False: + return pep8.Checker.report_error(self, + line_number, offset, text, check) + + # html generation + print('<tr><td>{0}</td><td>{1}</td></tr>'.format(line_number, text)) + + +if __name__ == '__main__': + + def usage(): + print('Usage: python pep8kivy.py [-html] <file_or_folder_to_check>*') + print('Folders will be checked recursively.') + sys.exit(1) + + if len(sys.argv) < 2: + usage() + if sys.argv[1] == '-html': + if len(sys.argv) < 3: + usage() + else: + htmlmode = True + targets = sys.argv[-1].split() + elif sys.argv == 2: + targets = sys.argv[-1] + else: + targets = sys.argv[-1].split() + + def check(fn): + try: + checker = KivyStyleChecker(fn) + except IOError: + # File couldn't be opened, so was deleted apparently. + # Don't check deleted files. + return 0 + return checker.check_all() + + errors = 0 + exclude_dirs = ['/lib', '/coverage', '/pep8', '/doc'] + exclude_files = ['kivy/gesture.py', 'osx/build.py', 'win32/build.py', + 'kivy/tools/stub-gl-debug.py', + 'kivy/modules/webdebugger.py', + 'kivy/modules/_webdebugger.py'] + for target in targets: + if isdir(target): + if htmlmode: + path = join(dirname(abspath(__file__)), 'pep8base.html') + print(open(path, 'r').read()) + print('''<p>Generated: %s</p><table>''' % (time.strftime('%c'))) + + for dirpath, dirnames, filenames in walk(target): + cont = False + for pat in exclude_dirs: + if pat in dirpath: + cont = True + break + if cont: + continue + for filename in filenames: + if not filename.endswith('.py'): + continue + cont = False + complete_filename = join(dirpath, filename) + for pat in exclude_files: + if complete_filename.endswith(pat): + cont = True + if cont: + continue + + if htmlmode: + print('<tr><th colspan="2">%s</td></tr>' \ + % complete_filename) + errors += check(complete_filename) + + if htmlmode: + print('</div></div></table></body></html>') + + else: + # Got a single file to check + for pat in exclude_dirs + exclude_files: + if pat in target: + break + else: + if target.endswith('.py'): + errors += check(target) + + # If errors is 0 we return with 0. That's just fine. + sys.exit(errors) diff --git a/external/plyer/tools/pep8checker/pre-commit.githook b/external/plyer/tools/pep8checker/pre-commit.githook new file mode 100644 index 0000000..23d119c --- /dev/null +++ b/external/plyer/tools/pep8checker/pre-commit.githook @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +''' + Kivy Git Pre-Commit Hook to Enforce Styleguide + ============================================== + + This script is not supposed to be run directly. + Instead, copy it to your kivy/.git/hooks/ directory, call it 'pre-commit' + and make it executable. + + If you attempt to commit, git will run this script, which in turn will run + the styleguide checker over your code and abort the commit if there are any + errors. If that happens, please fix & retry. + + To install:: + + cp kivy/tools/pep8checker/pre-commit.githook .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit +''' + +import sys, os +from os.path import dirname, abspath, sep, join +from subprocess import call, Popen, PIPE + +curdir = dirname(abspath(__file__)) +kivydir = sep.join(curdir.split(sep)[:-2]) +srcdir = join(kivydir, 'kivy') +script = join(srcdir, 'tools', 'pep8checker', 'pep8kivy.py') +try: + with open(script): pass +except IOError: + # if this not the kivy project, find the script file in the kivy project + os.environ['KIVY_NO_CONSOLELOG'] = '1' + import kivy + script = join(dirname(kivy.__file__), 'tools', 'pep8checker', 'pep8kivy.py') + srcdir = '' + +# Only check the files that were staged +#proc = Popen(['git', 'diff', '--cached', '--name-only', 'HEAD'], stdout=PIPE) +#targets = [join(kivydir, target) for target in proc.stdout] + +# Correction: only check the files that were staged, but do not include +# deleted files. +proc = Popen(['git', 'diff', '--cached', '--name-status', 'HEAD'], stdout=PIPE) +proc.wait() + +# This gives output like the following: +# +# A examples/widgets/lists/list_simple_in_kv.py +# A examples/widgets/lists/list_simple_in_kv_2.py +# D kivy/uix/observerview.py +# +# So check for D entries and remove them from targets. +# +targets = [] +for target in proc.stdout: + parts = [p.strip() for p in target.split()] + if parts[0] != 'D': + targets.append(join(kivydir, target.decode(encoding='UTF-8'))) + +# Untested possibility: After making the changes above for removing deleted +# files from targets, saw also where the git diff call could be: +# +# git diff --cached --name-only --diff-filter=ACM +# (leaving off D) +# +# and we could then remove the special handling in python for targets above. + +call(['git', 'stash', 'save', '--keep-index', '--quiet']) +retval = call([sys.executable, script, srcdir] + targets) +call(['git', 'stash', 'pop', '--quiet']) + +if retval: + # There are styleguide violations + print("Error:", retval, "styleguide violation(s) encountered!") + print("Your commit has been aborted. Please fix the violations and retry.") + sys.exit(retval) + diff --git a/external/plyer/utils.py b/external/plyer/utils.py new file mode 100644 index 0000000..347c8d5 --- /dev/null +++ b/external/plyer/utils.py @@ -0,0 +1,135 @@ +''' +Utils +===== + +''' + +__all__ = ('platform', ) + +from os import environ +from os import path +from sys import platform as _sys_platform + + +class Platform(object): + # refactored to class to allow module function to be replaced + # with module variable + + def __init__(self): + self._platform_ios = None + self._platform_android = None + + def __eq__(self, other): + return other == self._get_platform() + + def __ne__(self, other): + return other != self._get_platform() + + def __str__(self): + return self._get_platform() + + def __repr__(self): + return 'platform name: \'{platform}\' from: \n{instance}'.format( + platform=self._get_platform(), + instance=super(Platform, self).__repr__() + ) + + def __hash__(self): + return self._get_platform().__hash__() + + def _get_platform(self): + + if self._platform_android is None: + # ANDROID_ARGUMENT and ANDROID_PRIVATE are 2 environment variables + # from python-for-android project + self._platform_android = 'ANDROID_ARGUMENT' in environ + + if self._platform_ios is None: + self._platform_ios = (environ.get('KIVY_BUILD', '') == 'ios') + + # On android, _sys_platform return 'linux2', so prefer to check the + # import of Android module than trying to rely on _sys_platform. + if self._platform_android is True: + return 'android' + elif self._platform_ios is True: + return 'ios' + elif _sys_platform in ('win32', 'cygwin'): + return 'win' + elif _sys_platform == 'darwin': + return 'macosx' + elif _sys_platform[:5] == 'linux': + return 'linux' + return 'unknown' + + +platform = Platform() + + +class Proxy(object): + # taken from http://code.activestate.com/recipes/496741-object-proxying/ + + __slots__ = ['_obj', '_name', '_facade'] + + def __init__(self, name, facade): + object.__init__(self) + object.__setattr__(self, '_obj', None) + object.__setattr__(self, '_name', name) + object.__setattr__(self, '_facade', facade) + + def _ensure_obj(self): + obj = object.__getattribute__(self, '_obj') + if obj: + return obj + # do the import + try: + name = object.__getattribute__(self, '_name') + module = 'plyer.platforms.{}.{}'.format( + platform, name) + mod = __import__(module, fromlist='.') + obj = mod.instance() + except: + import traceback + traceback.print_exc() + facade = object.__getattribute__(self, '_facade') + obj = facade() + + object.__setattr__(self, '_obj', obj) + return obj + + def __getattribute__(self, name): + if name == '__doc__': + return + object.__getattribute__(self, '_ensure_obj')() + return getattr(object.__getattribute__(self, '_obj'), name) + + def __delattr__(self, name): + object.__getattribute__(self, '_ensure_obj')() + delattr(object.__getattribute__(self, '_obj'), name) + + def __setattr__(self, name, value): + object.__getattribute__(self, '_ensure_obj')() + setattr(object.__getattribute__(self, '_obj'), name, value) + + def __bool__(self): + object.__getattribute__(self, '_ensure_obj')() + return bool(object.__getattribute__(self, '_obj')) + + def __str__(self): + object.__getattribute__(self, '_ensure_obj')() + return str(object.__getattribute__(self, '_obj')) + + def __repr__(self): + object.__getattribute__(self, '_ensure_obj')() + return repr(object.__getattribute__(self, '_obj')) + + +def whereis_exe(program): + ''' Tries to find the program on the system path. + Returns the path if it is found or None if it's not found. + ''' + path_split = ';' if platform == 'win' else ':' + for p in environ.get('PATH', '').split(path_split): + if path.exists(path.join(p, program)) and \ + not path.isdir(path.join(p, program)): + return path.join(p, program) + return None diff --git a/external/plyer/utils.pyc b/external/plyer/utils.pyc Binary files differnew file mode 100644 index 0000000..ff2d89a --- /dev/null +++ b/external/plyer/utils.pyc |