├── .coveragerc ├── .gitignore ├── LICENCE ├── README.md ├── __main__.py ├── make_release.py ├── notes ├── README.md ├── lessons.txt └── todo.txt ├── release └── vimswitch ├── requirements.txt ├── run_tests.bat ├── run_tests.sh ├── scripts ├── __init__.py └── pack.py ├── setup.py ├── setup_py2exe.py ├── tox.ini └── vimswitch ├── Action.py ├── ActionResolver.py ├── Application.py ├── ApplicationDirs.py ├── CommandLineParser.py ├── ConfigFile.py ├── DiskIo.py ├── FileDownloader.py ├── GithubZipballExtractor.py ├── InvalidArgsAction.py ├── Profile.py ├── ProfileCache.py ├── ProfileCopier.py ├── ProfileDataIo.py ├── ProfileRetriever.py ├── ProfileUrlResolver.py ├── Settings.py ├── ShowCurrentProfileAction.py ├── ShowVersionAction.py ├── SwitchProfileAction.py ├── UpdateProfileAction.py ├── VimSwitch.py ├── __init__.py ├── six.py ├── test ├── BaseTestCase.py ├── CommonDiskIoTests.py ├── FakeFileDownloader.py ├── FileSystemSandbox.py ├── FileSystemTestCase.py ├── SimpleServer.py ├── Stubs.py ├── TestDiskIo.py ├── TestHelpers.py ├── __init__.py ├── data │ ├── fake_internet │ │ ├── README │ │ ├── https.github.com.test.vimrc.archive │ │ │ └── master.zip │ │ └── https.github.com.test2.vimrc.archive │ │ │ └── master.zip │ ├── fake_internet2 │ │ ├── README │ │ └── https.github.com.test.vimrc.archive │ │ │ └── master.zip │ ├── github_zipball_multiple_root_dirs.zip │ ├── github_zipball_no_root_dir.zip │ ├── home │ │ ├── .vim │ │ │ └── plugin │ │ │ │ └── dummy_plugin.vim │ │ └── .vimrc │ ├── simple.txt │ ├── simple.zip │ ├── vimrc-master.zip │ └── vimswitchrc ├── test_ActionResolver.py ├── test_ApplicationDirs.py ├── test_CommandLineParser.py ├── test_ConfigFile.py ├── test_FileDownloader.py ├── test_GithubZipballExtractor.py ├── test_Profile.py ├── test_ProfileCache.py ├── test_ProfileCopier.py ├── test_ProfileDataIo.py ├── test_ProfileRetriever.py ├── test_ProfileUrlResolver.py ├── test_ShowCurrentProfileAction.py ├── test_SwitchProfileAction.py ├── test_UpdateProfileAction.py ├── test_VimSwitch.py ├── test_ZipFile.py └── workingDir │ └── README └── version.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | include = vimswitch/* 5 | omit = vimswitch/six.py 6 | 7 | [report] 8 | ignore_errors = True -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipynb 2 | *.pyc 3 | .coverage 4 | .ropeproject 5 | .tox 6 | build/ 7 | cover/ 8 | dist/ 9 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VimSwitch 2 | 3 | For an overview and installation steps visit [https://priomsrb.github.io/vimswitch/](https://priomsrb.github.io/vimswitch/). 4 | 5 | ## Requirements 6 | Running VimSwitch only requires Python 2.7 or 3.2+. However, if you want to build and test VimSwitch you need to install its dependencies using `pip install -r requirements.txt` 7 | 8 | ## Testing 9 | VimSwitch needs to support multiple Python versions over multiple operating systems. Therefore, it's important to maintain a high test coverage. 10 | 11 | To run the test suite against the current Python version use `run_tests.sh` or `run_tests.bat`. 12 | 13 | To run tests against all supported versions of Python use `tox`. 14 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | from vimswitch.VimSwitch import VimSwitch 2 | import sys 3 | 4 | 5 | if __name__ == '__main__': 6 | vimSwitch = VimSwitch() 7 | exitCode = vimSwitch.main(sys.argv) 8 | sys.exit(exitCode) 9 | -------------------------------------------------------------------------------- /make_release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from scripts.pack import pack 4 | 5 | if __name__ == '__main__': 6 | # Create the `vimswitch` executable inside the `release` folder. 7 | # The executable will only contain the vimswitch application code i.e. the 8 | # test code will not be included. 9 | 10 | sources = [ 11 | '__main__.py', 12 | 'vimswitch/*.py' 13 | ] 14 | 15 | pack(sources, 'release/vimswitch') 16 | -------------------------------------------------------------------------------- /notes/README.md: -------------------------------------------------------------------------------- 1 | These are mostly personal notes related to the project. They are not very organized and will often be out of date. -------------------------------------------------------------------------------- /notes/lessons.txt: -------------------------------------------------------------------------------- 1 | - When doing end to end tests, make sure to clear the application state between calls to app.main(). Clearing stdout is also needed if stdout is being asserted on. 2 | -------------------------------------------------------------------------------- /notes/todo.txt: -------------------------------------------------------------------------------- 1 | #On back: 2 | 3 | #Todo: 4 | - The --help option should not have a exit code of -1 5 | - Create an .exe version. Check py2exe mailing list 6 | - Add chmod and other file status operations to DiskSandbox 7 | - Write test for switching to the current profile 8 | - Write test for switching to default profile on first run 9 | - Write test for __main__.py. Check that it returns an exit code 10 | - Create a Logger class. Use for logging exceptions 11 | - Try using mock.patch for making operations safe in FileSystemTestCase 12 | - Use os.path.commonprefix([requested_path, startdir]) instead of string.startswith 13 | - Find out how to prevent path traversal by using os.path.relpath 14 | - Use u'' or b'' instead of plain '' for strings 15 | - Make VimSwitch.main() print stack traces when given the --verbose option. 16 | - FileSystemSandbox uses startsWith. What happens if we try to write to workingDir2? or try to delete workingDir itself? 17 | 18 | #Cancelled: 19 | - Maybe move getProfileUrl to be a method of Profile. [Maybe later when profiles can have custom URLs] 20 | - Migrate FileSystemTestCase's to use FakeDiskIo. [Does not model DiskIo.deleteDir(path,force=False) properly. So will not be used anymore] 21 | - Find places where we do 'import .Module'. Replace with 'from .Module import foo' 22 | - Do a grep for 'pass'. May be some incomplete methods 23 | - Create a ConfigFiles class with createConfigFiles [Created ApplicationDirs instead] 24 | ? Move the DiskIo tests into an integration test suite [It's fast enough as it is] 25 | - Get some naming consistency: 26 | % Use 'path' instead of filename or dir 27 | - Some tests contain 'when' some don't. Make it consistent 28 | - Rename tests from 'test_*.py' to 'Test*.py'. Make it consistent with the class name 29 | 30 | 31 | #Porting: 32 | - Never use bare ''. Always prefix with u'' or b'' 33 | - investigate tox - testing different versions of python 34 | 35 | 36 | Version 0.#1: 37 | % Show current profile when run with no arguments 38 | % Overwrite to cached profile when switching away 39 | % Print a message when copying profile to cache. 'Saving profile: default' 40 | % Allow updating cached profile. 'vimswitch --update test/vimrc' 41 | % Make a script for packaging VimSwitch into a single executable file 42 | 43 | Version 0.#2: 44 | - Allow launching vim with a profile without changing current profile (vimswitch --launch test/vimrc) 45 | 46 | #Done: 47 | - rename Settings.getDefaultProfileName() to getDefaultProfile? 48 | - Use os.path.join instead of appending '/' 49 | - Rename the settings functions to plain variables instead of getters 50 | - Write tests for TestFileDownloader 51 | - Make test for exception when FileDownloader is given a 404 URL 52 | - Make TestFileDownloader throw error on 404's [http://stackoverflow.com/questions/1308542/how-to-catch-404-error-in-urllib-urlretrieve] 53 | - Move TestFileDownloader into a new test suite: 'slow' 54 | - Commit the FileDownloader changes 55 | - Make os.makedirs safe in FileSystemTestCase 56 | - Make TestProfileCache into a FileSystemTestCase 57 | - Add actions for shortHelp 58 | - Create default settings files 59 | - Find a way for vimswitch/test/workingDir to be added to git. Otherwise generate it during tests. [Added a README file inside it] 60 | - Write VimSwitch tests for no args and invalid args. 61 | - Use PyFakeFS to make FakeFsDiskIo 62 | - Write a test for DiskIo.removeDir(force=True) removing read only files too. Then implement. 63 | - Use force on rmtree to remove read only files too. Useful because some .git files can be read only 64 | - Write tests for when ~/.vim is read only. Should work as expected 65 | - Write a test for VimSwitch: test_switchProfile_createsApplicationDirs 66 | - Create an end-to-end test for VimSwitch 67 | - Maybe use the six module in tests for easier porting to python3 68 | - rename ProfileCache.getLocation to getProfileLocation 69 | - Rename ShortHelpAction to showCurrentProfileAction 70 | - Write tests for ShortHelpAction 71 | - Inline the settings stubs 72 | - Add a common testcase class. Add assertNotRegex to it 73 | - Find a way to use fake_internet2 in TestVimSwitch.test_updateProfile_redownloadsCachedProfile 74 | - Add more assertPathDoesNotExist in TestVimSwitch 75 | - Make Actions inherit from Action. Refactor ResolveActions to use a common action object. 76 | - Move the sandbox stuff in FileSystemTestCase into new DiskSandbox class 77 | - make Action.execute() an abstract method. (raise NotImplementedError). See if we have any other abstract methods. 78 | - Upload my vim profile to github 79 | - Test vimswitch on Ma's (Linux) and Baba's (Windows) pc's. 80 | - Add a --version option 81 | - Add descriptions to --update and --version options 82 | -------------------------------------------------------------------------------- /release/vimswitch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/release/vimswitch -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | mock 3 | tox 4 | coverage 5 | -------------------------------------------------------------------------------- /run_tests.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: TEST_TYPE - Possible values: 4 | :: BASIC = Runs all tests except the slow and external ones 5 | :: ALL = Runs the basic and slow tests. Slow tests are those that create local 6 | :: servers etc. 7 | :: EXTERNAL = Runs only external test. These tests make use of resources 8 | :: outside the local machine. For example downloading a file from github. 9 | :: Avoid running these tests frequently; we want to be nice to others :) 10 | :: COVERAGE = Checks the test coverage after running ALL tests 11 | :: CUSTOM = Use this for running specific tests 12 | 13 | set TEST_TYPE=BASIC 14 | 15 | :loop 16 | cls 17 | echo %TEST_TYPE% TESTS 18 | python --version 19 | 20 | if "%TEST_TYPE%"=="BASIC" ( 21 | :: Add --nocapture to show stdout 22 | nosetests -a "!slow,!external,!skip" 23 | ) 24 | if "%TEST_TYPE%"=="ALL" ( 25 | nosetests -a "!external,!skip" 26 | ) 27 | if "%TEST_TYPE%"=="EXTERNAL" ( 28 | nosetests -a "external,!skip" 29 | ) 30 | if "%TEST_TYPE%"=="COVERAGE" ( 31 | nosetests -a "!external,!skip" --with-coverage --cover-package=vimswitch --cover-branches 32 | ) 33 | if "%TEST_TYPE%"=="CUSTOM" ( 34 | rem nosetests vimswitch.test.. 35 | ) 36 | 37 | pause 38 | goto loop 39 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # TEST_TYPE - Possible values: 4 | # BASIC = Runs all tests except the slow and external ones 5 | # ALL = Runs the basic and slow tests. Slow tests are those that create local 6 | # servers etc. 7 | # EXTERNAL = Runs only external test. These tests make use of resources 8 | # outside the local machine. For example downloading a file from github. 9 | # Avoid running these tests frequently; we want to be nice to others :) 10 | # COVERAGE = Checks the test coverage after running ALL tests 11 | # CUSTOM = Use this for running specific tests 12 | TEST_TYPE=BASIC 13 | 14 | while true; do 15 | clear 16 | echo $TEST_TYPE TESTS 17 | python --version 18 | 19 | case $TEST_TYPE in 20 | "BASIC") 21 | # Add --nocapture to show stdout 22 | nosetests -a "!slow,!external,!skip" 23 | ;; 24 | "ALL") 25 | nosetests -a "!external,!skip" 26 | ;; 27 | "EXTERNAL") 28 | nosetests -a "external,!skip" 29 | ;; 30 | "COVERAGE") 31 | nosetests -a "!external,!skip" --with-coverage --cover-package=vimswitch --cover-branches --cover-html 32 | ;; 33 | "CUSTOM") 34 | #nosetests vimswitch.test.. 35 | ;; 36 | esac 37 | 38 | read -p "Press Enter to continue..." 39 | done 40 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/pack.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import glob 3 | import os 4 | import stat 5 | 6 | 7 | def pack(sources, output): 8 | """ 9 | Creates a single file python executable containing the given sources. 10 | 11 | `sources` is a list of filenames or globs. One of the entries must be a 12 | __main__.py file. 13 | 14 | `output` is the filename that will be given to the executable. 15 | 16 | For example: 17 | pack(['__main__.py', 'myproject/*.py'], 'myproject']) 18 | This will create an executable called 'myproject'. This executable can be 19 | run using: 20 | ./myproject 21 | or 22 | python myproject 23 | """ 24 | files = _expandGlobs(sources) 25 | _zipFiles(files, output) 26 | _addShebang(output) 27 | _makeExecutable(output) 28 | fileSize = int(_getFileSize(output) / 1024) 29 | print('Executable created: %s (%skb)' % (output, fileSize)) 30 | 31 | 32 | def _expandGlobs(globs): 33 | expandedGlobs = [] 34 | for g in globs: 35 | expandedGlobs += glob.glob(g) 36 | return expandedGlobs 37 | 38 | 39 | def _zipFiles(filenames, output): 40 | print("Zipping files: ") 41 | with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED) as outputZip: 42 | for filename in filenames: 43 | print('\t%s' % filename) 44 | outputZip.write(filename) 45 | 46 | 47 | def _addShebang(filename): 48 | """ 49 | Since python 2.6, python is able to execute a zip file if they contain a 50 | __main__.py file inside it. By adding the python shebang at the top of 51 | the zip file, python will attempt to execute this file. 52 | 53 | Conveniently, zip files can be prepended with data and still be valid. So 54 | what we end up with is a self-executing zip file. 55 | """ 56 | shebang = b'#!/usr/bin/env python' 57 | print('Adding shebang: %s' % shebang.decode('utf-8')) 58 | with open(filename, 'rb+') as f: 59 | previousContent = f.read() 60 | f.seek(0) 61 | f.write(shebang) 62 | f.write(b'\n') 63 | f.write(previousContent) 64 | 65 | 66 | def _getFileSize(filename): 67 | return os.path.getsize(filename) 68 | 69 | 70 | def _makeExecutable(filename): 71 | print('Setting executable flag') 72 | st = os.stat(filename) 73 | os.chmod(filename, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # This file is only used by tox right now. So the values might not all be 2 | # correct. 3 | 4 | from distutils.core import setup 5 | 6 | setup( 7 | name='vimswitch', 8 | version='0.1', 9 | description='A utility for switching between vim profiles', 10 | author='Shafqat Bhuiyan', 11 | author_email='priomsrb@gmail.com', 12 | url='https://github.com/priomsrb/vimswitch', 13 | packages=['vimswitch', 'vimswitch.test'], 14 | package_data={'vimswitch': ['vimswitch/*'], 'vimswitch.test': ['vimswitch/test/*']} 15 | #long_description='''Really long text here.''' 16 | # 17 | #This next part it for the Cheese Shop 18 | #classifiers=[] 19 | ) 20 | -------------------------------------------------------------------------------- /setup_py2exe.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import py2exe 3 | 4 | setup( 5 | console=[{ 6 | 'script': '__main__.py', 7 | 'dest_base': 'vimswitch' 8 | }], 9 | zipfile=None, 10 | options={"py2exe" : { 11 | "includes": ["urllib.request"], 12 | "excludes": ["doctest", "pdb", "unittest", "difflib", "inspect"], 13 | "bundle_files": 1, 14 | "compressed": True, 15 | "optimize": 2, 16 | #"dll_excludes": ["MSVCP90.dll"] 17 | }} 18 | ) -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27, py32, py33, py34 8 | 9 | [testenv] 10 | commands = nosetests -a "!slow,!external,!skip" 11 | deps = 12 | nose 13 | mock 14 | -------------------------------------------------------------------------------- /vimswitch/Action.py: -------------------------------------------------------------------------------- 1 | class Action: 2 | def __init__(self): 3 | self.exitCode = 0 4 | 5 | def execute(): 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /vimswitch/ActionResolver.py: -------------------------------------------------------------------------------- 1 | from .CommandLineParser import getCommandLineParser 2 | from .InvalidArgsAction import createInvalidArgsAction 3 | from .ShowCurrentProfileAction import createShowCurrentProfileAction 4 | from .ShowVersionAction import createShowVersionAction 5 | from .SwitchProfileAction import createSwitchProfileAction 6 | from .UpdateProfileAction import createUpdateProfileAction 7 | 8 | 9 | class ActionResolver: 10 | def __init__(self, app, commandLineParser): 11 | self.app = app 12 | self.commandLineParser = commandLineParser 13 | self.exitCode = 0 14 | 15 | def doActions(self): 16 | actionString = self.commandLineParser.action 17 | 18 | if actionString == 'switchProfile': 19 | action = createSwitchProfileAction(self.app) 20 | action.profile = self.commandLineParser.profile 21 | elif actionString == 'updateProfile': 22 | action = createUpdateProfileAction(self.app) 23 | action.profile = self.commandLineParser.profile 24 | elif actionString == 'showCurrentProfile': 25 | action = createShowCurrentProfileAction(self.app) 26 | elif actionString == 'showVersion': 27 | action = createShowVersionAction(self.app) 28 | else: 29 | action = createInvalidArgsAction(self.app) 30 | action.errorMessage = self.commandLineParser.errorMessage 31 | action.helpText = self.commandLineParser.helpText 32 | 33 | action.execute() 34 | self.exitCode = action.exitCode 35 | 36 | 37 | def getActionResolver(app): 38 | return app.get('actionResolver', createActionResolver(app)) 39 | 40 | 41 | def createActionResolver(app): 42 | commandLineParser = getCommandLineParser(app) 43 | actionResolver = ActionResolver(app, commandLineParser) 44 | return actionResolver 45 | -------------------------------------------------------------------------------- /vimswitch/Application.py: -------------------------------------------------------------------------------- 1 | class Application: 2 | """ 3 | A container for components that are used application-wide. 4 | 5 | Examples of how this class can be used: 6 | 7 | - Set a component 8 | `app.foo = myFoo` 9 | 10 | - Get (or create) a component: 11 | `foo = app.get('foo', Foo())` 12 | This will try to return `app.foo`. If that does not exist, then it will 13 | set `app.foo = Foo()` and return that instead. 14 | 15 | - Get an already created component 16 | `foo = app.foo` 17 | Use this when you are sure that app.foo has already been set 18 | """ 19 | 20 | def get(self, attr, default): 21 | """ 22 | Returns self.attr. If attr does not exist, then make self.attr = default 23 | and then return default. 24 | """ 25 | if hasattr(self, attr): 26 | return getattr(self, attr) 27 | else: 28 | setattr(self, attr, default) 29 | return default 30 | -------------------------------------------------------------------------------- /vimswitch/ApplicationDirs.py: -------------------------------------------------------------------------------- 1 | from .DiskIo import getDiskIo 2 | from .Settings import getSettings 3 | 4 | 5 | class ApplicationDirs: 6 | def __init__(self, settings, diskIo): 7 | self.settings = settings 8 | self.diskIo = diskIo 9 | 10 | def createIfNone(self): 11 | self._createDir(self.settings.configPath) 12 | self._createDir(self.settings.cachePath) 13 | self._createDir(self.settings.downloadsPath) 14 | 15 | def _createDir(self, path): 16 | if not self.diskIo.dirExists(path): 17 | self.diskIo.createDir(path) 18 | 19 | 20 | def getApplicationDirs(app): 21 | return app.get('applicationDirs', createApplicationDirs(app)) 22 | 23 | 24 | def createApplicationDirs(app): 25 | settings = getSettings(app) 26 | diskIo = getDiskIo(app) 27 | applicationDirs = ApplicationDirs(settings, diskIo) 28 | return applicationDirs 29 | -------------------------------------------------------------------------------- /vimswitch/CommandLineParser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from .Profile import Profile 3 | 4 | 5 | class CommandLineParser: 6 | def __init__(self): 7 | self.action = '' 8 | self.profile = None 9 | self.errorMessage = '' 10 | self.helpText = '' 11 | 12 | def parse(self, argv): 13 | parser = self._createParser() 14 | self.helpText = parser.format_help() 15 | 16 | argv = argv[1:] # Remove the program name from the arguments 17 | arguments = parser.parse_args(argv) 18 | 19 | self._processArguments(parser, arguments, argv) 20 | 21 | def _createParser(self): 22 | parser = CustomArgumentParser( 23 | prog='vimswitch', 24 | description='A utility for switching between vim profiles.') 25 | parser.add_argument('profile', nargs='?') 26 | parser.add_argument('-u', '--update', action='store_true', 27 | help='download profile again') 28 | parser.add_argument('-v', '--version', action='store_true', 29 | help='show version') 30 | return parser 31 | 32 | def _processArguments(self, parser, arguments, argv): 33 | if parser.hasError: 34 | self.errorMessage = parser.errorMessage 35 | self.action = 'invalidArgs' 36 | elif arguments.version: 37 | self.action = 'showVersion' 38 | elif arguments.update: 39 | self.action = 'updateProfile' 40 | profileName = arguments.profile 41 | if profileName is None: 42 | self.profile = None 43 | else: 44 | self.profile = Profile(profileName) 45 | elif arguments.profile is not None: 46 | self.action = 'switchProfile' 47 | profileName = arguments.profile 48 | self.profile = Profile(profileName) 49 | elif len(argv) == 0: 50 | self.action = 'showCurrentProfile' 51 | 52 | 53 | def getCommandLineParser(app): 54 | return app.get('commandLineParser', createCommandLineParser(app)) 55 | 56 | 57 | def createCommandLineParser(app): 58 | return CommandLineParser() 59 | 60 | 61 | class CustomArgumentParser(argparse.ArgumentParser): 62 | def __init__(self, *args, **kwargs): 63 | argparse.ArgumentParser.__init__(self, *args, **kwargs) 64 | self.hasError = False 65 | self.errorMessage = '' 66 | 67 | def error(self, message): 68 | self.hasError = True 69 | self.errorMessage = message 70 | # Disable automatic exiting 71 | return 72 | 73 | def print_help(self, *args, **kwargs): 74 | # Disable automatic printing 75 | return 76 | 77 | def exit(self, *args, **kwargs): 78 | # Disable automatic exiting 79 | return 80 | -------------------------------------------------------------------------------- /vimswitch/ConfigFile.py: -------------------------------------------------------------------------------- 1 | from .DiskIo import getDiskIo 2 | from .Profile import Profile 3 | from .six import StringIO 4 | from .six.moves.configparser import SafeConfigParser 5 | 6 | 7 | class ConfigFile: 8 | def __init__(self, diskIo): 9 | self.diskIo = diskIo 10 | 11 | def loadSettings(self, settings, path): 12 | """ 13 | Reads the config file at path and applies the values to settings. If 14 | path does not exist, then settings will be left unchanged. 15 | """ 16 | if self.diskIo.fileExists(path): 17 | settingsIo = StringIO(self.diskIo.getFileContents(path)) 18 | config = self._getConfigParser() 19 | config.readfp(settingsIo) 20 | 21 | currentProfileName = config.get('settings', 'currentProfile') 22 | if currentProfileName is None: 23 | settings.currentProfile = None 24 | else: 25 | settings.currentProfile = Profile(currentProfileName) 26 | 27 | def saveSettings(self, settings, path): 28 | config = self._getConfigParser() 29 | config.add_section('settings') 30 | if settings.currentProfile is None: 31 | config.set('settings', 'currentProfile', None) 32 | else: 33 | config.set('settings', 'currentProfile', settings.currentProfile.name) 34 | 35 | settingsIo = StringIO() 36 | config.write(settingsIo) 37 | self.diskIo.createFile(path, settingsIo.getvalue()) 38 | 39 | def _getConfigParser(self): 40 | # We set allow_no_value to True because settings.currentProfile can 41 | # sometimes be None 42 | configParser = SafeConfigParser(allow_no_value=True) 43 | return configParser 44 | 45 | 46 | def getConfigFile(app): 47 | return app.get('configFile', createConfigFile(app)) 48 | 49 | 50 | def createConfigFile(app): 51 | diskIo = getDiskIo(app) 52 | configFile = ConfigFile(diskIo) 53 | return configFile 54 | -------------------------------------------------------------------------------- /vimswitch/DiskIo.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import os 3 | import stat 4 | 5 | 6 | class DiskIo: 7 | 8 | def createFile(self, filePath, contents): 9 | with open(filePath, 'w') as file: 10 | file.write(contents) 11 | 12 | def getFileContents(self, filePath): 13 | with open(filePath, 'r') as file: 14 | return file.read() 15 | 16 | def copyFile(self, srcPath, destPath): 17 | shutil.copy(srcPath, destPath) 18 | 19 | def move(self, srcPath, destPath): 20 | shutil.move(srcPath, destPath) 21 | 22 | def deleteFile(self, filePath): 23 | os.remove(filePath) 24 | 25 | def fileExists(self, filePath): 26 | return os.path.isfile(filePath) 27 | 28 | def createDir(self, dirPath): 29 | return os.mkdir(dirPath) 30 | 31 | def createDirWithParents(self, dirPath): 32 | return os.makedirs(dirPath) 33 | 34 | def copyDir(self, srcPath, destPath): 35 | """ 36 | Recursively copies the src directory to a new dest directory. Creates 37 | the parent directories of the destination if required. 38 | 39 | Raises OSError if the destination directory already exists. 40 | """ 41 | return shutil.copytree(srcPath, destPath) 42 | 43 | def deleteDir(self, dirPath): 44 | """ 45 | Recursively delete a directory. Read-only files inside the directory 46 | will also be deleted. 47 | """ 48 | shutil.rmtree(dirPath, onerror=self._remove_readonly) 49 | 50 | def dirExists(self, dirPath): 51 | return os.path.isdir(dirPath) 52 | 53 | def isDirEmpty(self, dirPath): 54 | return len(os.listdir(dirPath)) == 0 55 | 56 | def getDirContents(self, dirPath): 57 | return os.listdir(dirPath) 58 | 59 | def anyExists(self, path): 60 | return self.fileExists(path) or self.dirExists(path) 61 | 62 | def setReadOnly(self, path, readOnly): 63 | st = os.stat(path) 64 | if readOnly: 65 | os.chmod(path, st.st_mode & ~stat.S_IWRITE) 66 | else: 67 | os.chmod(path, st.st_mode | stat.S_IWRITE) 68 | 69 | def isReadOnly(self, path): 70 | mode = os.stat(path)[stat.ST_MODE] 71 | return not mode & stat.S_IWRITE 72 | 73 | # Private 74 | 75 | def _remove_readonly(self, func, path, excinfo): 76 | os.chmod(path, stat.S_IWRITE) 77 | func(path) 78 | 79 | 80 | def getDiskIo(app): 81 | return app.get('diskIo', createDiskIo(app)) 82 | 83 | 84 | def createDiskIo(app): 85 | return DiskIo() 86 | -------------------------------------------------------------------------------- /vimswitch/FileDownloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from .six.moves.urllib_parse import urlparse 4 | from .six.moves.urllib_request import FancyURLopener 5 | import datetime 6 | from .Settings import getSettings 7 | from .DiskIo import getDiskIo 8 | 9 | 10 | class FileDownloader: 11 | def __init__(self, settings, diskIo): 12 | self.settings = settings 13 | self.diskIo = diskIo 14 | 15 | def download(self, url, path): 16 | """ 17 | Downloads the file at `url` saves it into `path`. If `path` is a directory, 18 | the file is saved inside that directory. If path points to a file then 19 | we save the download as that file. Returns the filename of the 20 | downloaded file. 21 | """ 22 | temporaryFilename = 'download_' + self._getTimestamp() + '.tmp' 23 | temporaryFilePath = os.path.join(self.settings.downloadsPath, temporaryFilename) 24 | (_, headers) = self.UrlOpener().retrieve(url, temporaryFilePath) 25 | filename = self._getDownloadFilename(url, headers) 26 | 27 | # If we are saving to a directory, then we add the file's basename to 28 | # the directory to get the full path 29 | if self.diskIo.dirExists(path): 30 | if filename != '': 31 | filePath = os.path.join(path, os.path.basename(filename)) 32 | else: 33 | # TODO: shouldn't filePath be inside path? 34 | filePath = temporaryFilePath 35 | else: 36 | # If we are saving to a file, just use that filename 37 | filePath = path 38 | 39 | # Move the temporary file to it's final name and destination 40 | self.diskIo.move(temporaryFilePath, filePath) 41 | 42 | return filePath 43 | 44 | def _getTimestamp(self): 45 | """ 46 | Returns a sortable timestamp. 47 | For example on Dec 31 2014 at 1:00:59pm this would return: 48 | 20141231_130059 49 | """ 50 | return datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 51 | 52 | def _getDownloadFilename(self, url, headers): 53 | """ 54 | Returns the filename of the download by first parsing the header and if 55 | unsuccessful, then parsing the url. If both fail, then returns an empty 56 | string. 57 | """ 58 | if 'content-disposition' in headers: 59 | regex = 'attachment; filename=(.*)' 60 | contentDisposition = headers['content-disposition'] 61 | match = re.match(regex, contentDisposition) 62 | if match is not None: 63 | filename = match.group(1) 64 | return filename 65 | 66 | urlPath = urlparse(url).path 67 | filename = os.path.basename(urlPath) 68 | return filename 69 | 70 | class UrlOpener(FancyURLopener): 71 | "A URLOpener that raises an error when there is a http error" 72 | def http_error_default(self, url, fp, errcode, errmsg, headers): 73 | errorMessage = 'Error when accessing %s: %s %s' % (url, errcode, errmsg) 74 | raise IOError(errorMessage) 75 | 76 | 77 | def getFileDownloader(app): 78 | return app.get('fileDownloader', createFileDownloader(app)) 79 | 80 | 81 | def createFileDownloader(app): 82 | settings = getSettings(app) 83 | diskIo = getDiskIo(app) 84 | fileDownloader = FileDownloader(settings, diskIo) 85 | return fileDownloader 86 | -------------------------------------------------------------------------------- /vimswitch/GithubZipballExtractor.py: -------------------------------------------------------------------------------- 1 | from .DiskIo import getDiskIo 2 | from zipfile import ZipFile 3 | import os 4 | 5 | 6 | class GithubZipballExtractor: 7 | def __init__(self, diskIo): 8 | self.diskIo = diskIo 9 | 10 | def extractZipball(self, zipFilename, path): 11 | ''' 12 | Github zipballs contain a root folder with the repo contents inside. 13 | This method extracts the contents of that root folder into a path. 14 | 15 | For example a typical github zipball might look like: 16 | myrepo-master.zip/ 17 | myrepo-master/ 18 | repo_file1 19 | repo_file2 20 | etc... 21 | 22 | This function will extract the repo contents directly into `path`: 23 | path/ 24 | repo_file1 25 | repo_file2 26 | etc... 27 | ''' 28 | ZipFile(zipFilename).extractall(path) 29 | rootDir = self._getRootDirName(zipFilename) 30 | rootDir = os.path.join(path, rootDir) 31 | self._moveDirContents(rootDir, path) 32 | self.diskIo.deleteDir(rootDir) 33 | 34 | def _moveDirContents(self, srcDir, destDir): 35 | dirContents = self.diskIo.getDirContents(srcDir) 36 | for item in dirContents: 37 | itemPath = os.path.join(srcDir, item) 38 | self.diskIo.move(itemPath, destDir) 39 | 40 | def _getRootDirName(self, zipFilename): 41 | zipContents = ZipFile(zipFilename).namelist() 42 | 43 | rootDirs = [dir for dir in zipContents if self._isZipRootDir(dir)] 44 | 45 | if len(rootDirs) > 1: 46 | raise IOError('Zipball %s has more than one root directory' % zipFilename) 47 | 48 | if len(rootDirs) < 1: 49 | raise IOError('Zipball %s has no root directory' % zipFilename) 50 | 51 | return rootDirs[0] 52 | 53 | def _isZipRootDir(self, path): 54 | if path.find('/') == len(path) - 1: 55 | return True 56 | else: 57 | return False 58 | 59 | 60 | def getGithubZipballExtractor(app): 61 | return app.get('githubZipballExtractor', createGithubZipballExtractor(app)) 62 | 63 | 64 | def createGithubZipballExtractor(app): 65 | diskIo = getDiskIo(app) 66 | githubZipballExtractor = GithubZipballExtractor(diskIo) 67 | return githubZipballExtractor 68 | -------------------------------------------------------------------------------- /vimswitch/InvalidArgsAction.py: -------------------------------------------------------------------------------- 1 | from .Action import Action 2 | 3 | 4 | class InvalidArgsAction(Action): 5 | def __init__(self): 6 | Action.__init__(self) 7 | self.errorMessage = '' 8 | self.helpText = '' 9 | 10 | def execute(self): 11 | print(self.errorMessage) 12 | print(self.helpText) 13 | self.exitCode = -1 14 | 15 | 16 | def createInvalidArgsAction(app): 17 | return InvalidArgsAction() 18 | -------------------------------------------------------------------------------- /vimswitch/Profile.py: -------------------------------------------------------------------------------- 1 | class Profile: 2 | def __init__(self, name): 3 | self.name = name 4 | 5 | def getName(self): 6 | return self.name 7 | 8 | def getDirName(self): 9 | return self.name.replace('/', '.') 10 | 11 | def __eq__(self, other): 12 | return isinstance(other, Profile) and self.name == other.name 13 | 14 | def __ne__(self, other): 15 | return not self.__eq__(other) 16 | 17 | def __repr__(self): 18 | return "Profile('%s')" % self.name 19 | -------------------------------------------------------------------------------- /vimswitch/ProfileCache.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .Settings import getSettings 3 | from .DiskIo import getDiskIo 4 | 5 | 6 | class ProfileCache: 7 | def __init__(self, settings, diskIo): 8 | self.settings = settings 9 | self.diskIo = diskIo 10 | 11 | def contains(self, profile): 12 | profileLocation = self.getProfileLocation(profile) 13 | return self.diskIo.dirExists(profileLocation) 14 | 15 | def delete(self, profile): 16 | profileDirPath = self.getProfileLocation(profile) 17 | self.diskIo.deleteDir(profileDirPath) 18 | 19 | def getProfileLocation(self, profile): 20 | """Returns the path where profile is located""" 21 | fullPath = os.path.join(self.settings.cachePath, profile.getDirName()) 22 | return os.path.normpath(fullPath) 23 | 24 | def createEmptyProfile(self, profile): 25 | location = self.getProfileLocation(profile) 26 | self.diskIo.createDir(location) 27 | 28 | 29 | def getProfileCache(app): 30 | return app.get('profileCache', createProfileCache(app)) 31 | 32 | 33 | def createProfileCache(app): 34 | settings = getSettings(app) 35 | diskIo = getDiskIo(app) 36 | profileCache = ProfileCache(settings, diskIo) 37 | return profileCache 38 | -------------------------------------------------------------------------------- /vimswitch/ProfileCopier.py: -------------------------------------------------------------------------------- 1 | from .Settings import getSettings 2 | from .ProfileCache import getProfileCache 3 | from .ProfileDataIo import getProfileDataIo 4 | 5 | 6 | class ProfileCopier: 7 | def __init__(self, settings, profileCache, profileDataIo): 8 | self.settings = settings 9 | self.profileCache = profileCache 10 | self.profileDataIo = profileDataIo 11 | 12 | def copyToHome(self, profile): 13 | """ 14 | Copies the cached profile to home, thus making it active 15 | """ 16 | # TODO: If profileDataIo.copy fails, then we are left with an empty 17 | # profile at home. So we should use the 'operate on temp then rename' 18 | # pattern 19 | homePath = self.settings.homePath 20 | profilePath = self.profileCache.getProfileLocation(profile) 21 | self.profileDataIo.delete(homePath) 22 | self.profileDataIo.copy(profilePath, homePath) 23 | 24 | def copyFromHome(self, profile): 25 | """ 26 | Copies the active profile data at home to a specified profile in cache. 27 | If the profile already exists in cache, it gets overwritten. 28 | """ 29 | if not self.profileCache.contains(profile): 30 | self.profileCache.createEmptyProfile(profile) 31 | profilePath = self.profileCache.getProfileLocation(profile) 32 | homePath = self.settings.homePath 33 | self.profileDataIo.delete(profilePath) 34 | self.profileDataIo.copy(homePath, profilePath) 35 | 36 | 37 | def getProfileCopier(app): 38 | return app.get('profileCopier', createProfileCopier(app)) 39 | 40 | 41 | def createProfileCopier(app): 42 | settings = getSettings(app) 43 | profileCache = getProfileCache(app) 44 | profileDataIo = getProfileDataIo(app) 45 | profileCopier = ProfileCopier(settings, profileCache, profileDataIo) 46 | return profileCopier 47 | -------------------------------------------------------------------------------- /vimswitch/ProfileDataIo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .Settings import getSettings 3 | from .DiskIo import getDiskIo 4 | 5 | 6 | class ProfileDataIo: 7 | def __init__(self, settings, diskIo): 8 | self.settings = settings 9 | self.diskIo = diskIo 10 | 11 | def delete(self, path): 12 | """Deletes profile data found at path""" 13 | for file in self.settings.profileFiles: 14 | filePath = os.path.join(path, file) 15 | if self.diskIo.fileExists(filePath): 16 | self.diskIo.deleteFile(filePath) 17 | 18 | for dir in self.settings.profileDirs: 19 | dirPath = os.path.join(path, dir) 20 | if self.diskIo.dirExists(dirPath): 21 | self.diskIo.deleteDir(dirPath) 22 | 23 | def copy(self, srcPath, destPath): 24 | """ 25 | Copies profile data from srcPath to destPath. Will replace existing 26 | files at destPath. 27 | """ 28 | for file in self.settings.profileFiles: 29 | srcFilePath = os.path.join(srcPath, file) 30 | destFilePath = os.path.join(destPath, file) 31 | if self.diskIo.fileExists(srcFilePath): 32 | self.diskIo.copyFile(srcFilePath, destFilePath) 33 | 34 | for dir in self.settings.profileDirs: 35 | srcDirPath = os.path.join(srcPath, dir) 36 | destDirPath = os.path.join(destPath, dir) 37 | if self.diskIo.dirExists(srcDirPath): 38 | self.diskIo.copyDir(srcDirPath, destDirPath) 39 | 40 | 41 | def getProfileDataIo(app): 42 | return app.get('profileDataIo', createProfileDataIo(app)) 43 | 44 | 45 | def createProfileDataIo(app): 46 | settings = getSettings(app) 47 | diskIo = getDiskIo(app) 48 | profileDataIo = ProfileDataIo(settings, diskIo) 49 | return profileDataIo 50 | -------------------------------------------------------------------------------- /vimswitch/ProfileRetriever.py: -------------------------------------------------------------------------------- 1 | from .DiskIo import getDiskIo 2 | from .FileDownloader import getFileDownloader 3 | from .GithubZipballExtractor import getGithubZipballExtractor 4 | from .ProfileCache import getProfileCache 5 | from .ProfileUrlResolver import getProfileUrl 6 | from .Settings import getSettings 7 | import os 8 | 9 | 10 | class ProfileRetriever: 11 | def __init__(self, settings, fileDownloader, profileCache, diskIo, githubZipballExtractor): 12 | self.settings = settings 13 | self.fileDownloader = fileDownloader 14 | self.profileCache = profileCache 15 | self.diskIo = diskIo 16 | self.githubZipballExtractor = githubZipballExtractor 17 | 18 | def retrieve(self, profile): 19 | """ 20 | Downloads a profile into the cache. If the profile already exists, then 21 | it is overwritten. 22 | """ 23 | url = getProfileUrl(profile) 24 | print('Downloading profile from %s' % url) 25 | downloadsPath = self.settings.downloadsPath 26 | downloadedFilePath = self.fileDownloader.download(url, downloadsPath) 27 | extractionDir = os.path.splitext(downloadedFilePath)[0] 28 | self.githubZipballExtractor.extractZipball(downloadedFilePath, extractionDir) 29 | 30 | if self.profileCache.contains(profile): 31 | self.profileCache.delete(profile) 32 | 33 | profileDir = self.profileCache.getProfileLocation(profile) 34 | self.diskIo.move(extractionDir, profileDir) 35 | 36 | 37 | def getProfileRetriever(app): 38 | return app.get('profileRetriever', createProfileRetriever(app)) 39 | 40 | 41 | def createProfileRetriever(app): 42 | settings = getSettings(app) 43 | fileDownloader = getFileDownloader(app) 44 | profileCache = getProfileCache(app) 45 | diskIo = getDiskIo(app) 46 | githubZipballExtractor = getGithubZipballExtractor(app) 47 | profileRetriever = ProfileRetriever(settings, fileDownloader, profileCache, diskIo, githubZipballExtractor) 48 | return profileRetriever 49 | -------------------------------------------------------------------------------- /vimswitch/ProfileUrlResolver.py: -------------------------------------------------------------------------------- 1 | def getProfileUrl(profile): 2 | prefix = 'https://github.com/' 3 | suffix = '/archive/master.zip' 4 | url = prefix + profile.getName() + suffix 5 | return url 6 | -------------------------------------------------------------------------------- /vimswitch/Settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .Profile import Profile 3 | 4 | 5 | class Settings: 6 | 7 | def __init__(self, homePath=''): 8 | """ 9 | homePath is used as the prefix for all other paths. If homePath is not 10 | specified it gets set to the user's home directory. 11 | """ 12 | self.homePath = homePath or os.path.expanduser('~') 13 | self.configPath = os.path.join(self.homePath, '.vimswitch') 14 | self.configFilePath = os.path.join(self.configPath, 'vimswitchrc') 15 | self.cachePath = os.path.join(self.configPath, 'profiles') 16 | self.downloadsPath = os.path.join(self.configPath, 'downloads') 17 | self.profileFiles = ['.vimrc', '_vimrc'] 18 | self.profileDirs = ['.vim', '_vim'] 19 | self.defaultProfile = Profile('default') 20 | self.currentProfile = None 21 | 22 | def __eq__(self, other): 23 | return self.__dict__ == other.__dict__ 24 | 25 | 26 | def getSettings(app): 27 | return app.get('settings', createSettings(app)) 28 | 29 | 30 | def createSettings(app): 31 | return Settings() 32 | -------------------------------------------------------------------------------- /vimswitch/ShowCurrentProfileAction.py: -------------------------------------------------------------------------------- 1 | from .Action import Action 2 | from .Settings import getSettings 3 | 4 | 5 | class ShowCurrentProfileAction(Action): 6 | def __init__(self, settings): 7 | Action.__init__(self) 8 | self.settings = settings 9 | 10 | def execute(self): 11 | if self.settings.currentProfile is None: 12 | profileName = 'None' 13 | else: 14 | profileName = self.settings.currentProfile.name 15 | 16 | message = 'Current profile: %s' % profileName 17 | 18 | print(message.strip()) 19 | 20 | 21 | def createShowCurrentProfileAction(app): 22 | settings = getSettings(app) 23 | showCurrentProfileAction = ShowCurrentProfileAction(settings) 24 | return showCurrentProfileAction 25 | -------------------------------------------------------------------------------- /vimswitch/ShowVersionAction.py: -------------------------------------------------------------------------------- 1 | from .Action import Action 2 | import platform 3 | import vimswitch.version 4 | 5 | 6 | class ShowVersionAction(Action): 7 | def __init__(self): 8 | Action.__init__(self) 9 | 10 | def execute(self): 11 | appVersion = vimswitch.version.__version__ 12 | pythonVersion = platform.python_version() 13 | 14 | print("vimswitch %s (python %s)" % (appVersion, pythonVersion)) 15 | 16 | 17 | def createShowVersionAction(app): 18 | return ShowVersionAction() 19 | -------------------------------------------------------------------------------- /vimswitch/SwitchProfileAction.py: -------------------------------------------------------------------------------- 1 | from .Action import Action 2 | from .ProfileCache import getProfileCache 3 | from .ProfileCopier import getProfileCopier 4 | from .ProfileRetriever import getProfileRetriever 5 | from .Settings import getSettings 6 | 7 | 8 | class SwitchProfileAction(Action): 9 | 10 | def __init__(self, settings, profileCache, profileCopier, profileRetriever): 11 | Action.__init__(self) 12 | self.settings = settings 13 | self.profileCache = profileCache 14 | self.profileCopier = profileCopier 15 | self.profileRetriever = profileRetriever 16 | self.update = False 17 | self.profile = None 18 | 19 | def execute(self): 20 | self._saveCurrentProfile() 21 | self._retrieveProfile(self.profile) 22 | self.profileCopier.copyToHome(self.profile) 23 | self.settings.currentProfile = self.profile 24 | print('Switched to profile: %s' % self.profile.name) 25 | 26 | def _saveCurrentProfile(self): 27 | currentProfile = self._getCurrentProfile() 28 | print('Saving profile: %s' % currentProfile.name) 29 | self.profileCopier.copyFromHome(currentProfile) 30 | 31 | def _retrieveProfile(self, profile): 32 | if not self.profileCache.contains(profile) or self.update: 33 | self.profileRetriever.retrieve(profile) 34 | 35 | def _getCurrentProfile(self): 36 | if self.settings.currentProfile is None: 37 | currentProfile = self.settings.defaultProfile 38 | else: 39 | currentProfile = self.settings.currentProfile 40 | return currentProfile 41 | 42 | 43 | def createSwitchProfileAction(app): 44 | settings = getSettings(app) 45 | profileCache = getProfileCache(app) 46 | profileCopier = getProfileCopier(app) 47 | profileRetriever = getProfileRetriever(app) 48 | switchProfileAction = SwitchProfileAction(settings, profileCache, profileCopier, profileRetriever) 49 | return switchProfileAction 50 | -------------------------------------------------------------------------------- /vimswitch/UpdateProfileAction.py: -------------------------------------------------------------------------------- 1 | from .Action import Action 2 | from .Settings import getSettings 3 | from .SwitchProfileAction import createSwitchProfileAction 4 | 5 | 6 | class UpdateProfileAction(Action): 7 | def __init__(self, settings, switchProfileAction): 8 | Action.__init__(self) 9 | self.settings = settings 10 | self.switchProfileAction = switchProfileAction 11 | self.profile = None 12 | 13 | def execute(self): 14 | self.profile = self._getProfile() 15 | 16 | if self.profile == self.settings.defaultProfile: 17 | print('Cannot update default profile') 18 | self.exitCode = -1 19 | return 20 | 21 | self.switchProfileAction.update = True 22 | self.switchProfileAction.profile = self.profile 23 | self.switchProfileAction.execute() 24 | 25 | def _getProfile(self): 26 | if self.profile is None: 27 | if self.settings.currentProfile is None: 28 | return self.settings.defaultProfile 29 | else: 30 | return self.settings.currentProfile 31 | else: 32 | return self.profile 33 | 34 | 35 | def createUpdateProfileAction(app): 36 | settings = getSettings(app) 37 | switchProfileAction = createSwitchProfileAction(app) 38 | updateProfileAction = UpdateProfileAction(settings, switchProfileAction) 39 | return updateProfileAction 40 | -------------------------------------------------------------------------------- /vimswitch/VimSwitch.py: -------------------------------------------------------------------------------- 1 | from .ActionResolver import getActionResolver 2 | from .Application import Application 3 | from .ApplicationDirs import getApplicationDirs 4 | from .CommandLineParser import getCommandLineParser 5 | from .ConfigFile import getConfigFile 6 | from .Settings import getSettings 7 | 8 | 9 | class VimSwitch: 10 | def __init__(self, app=Application()): 11 | self.app = app 12 | self.raiseExceptions = False 13 | 14 | def main(self, argv): 15 | """ 16 | Runs VimSwitch with the given args. Returns 0 on success, or -1 if an 17 | error occurred. 18 | """ 19 | try: 20 | self.loadSettings() 21 | commandLineParser = getCommandLineParser(self.app) 22 | commandLineParser.parse(argv) 23 | actionResolver = getActionResolver(self.app) 24 | actionResolver.doActions() 25 | self.saveSettings() 26 | return actionResolver.exitCode 27 | except Exception as e: 28 | message = 'Error: %s' % str(e) 29 | print(message) 30 | return -1 31 | 32 | def loadSettings(self): 33 | applicationDirs = getApplicationDirs(self.app) 34 | applicationDirs.createIfNone() 35 | settings = getSettings(self.app) 36 | configFile = getConfigFile(self.app) 37 | configFile.loadSettings(settings, settings.configFilePath) 38 | 39 | def saveSettings(self): 40 | settings = getSettings(self.app) 41 | configFile = getConfigFile(self.app) 42 | configFile.saveSettings(settings, settings.configFilePath) 43 | -------------------------------------------------------------------------------- /vimswitch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/__init__.py -------------------------------------------------------------------------------- /vimswitch/six.py: -------------------------------------------------------------------------------- 1 | """Utilities for writing code that runs on Python 2 and 3""" 2 | 3 | # Copyright (c) 2010-2014 Benjamin Peterson 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import absolute_import 24 | 25 | import functools 26 | import operator 27 | import sys 28 | import types 29 | 30 | __author__ = "Benjamin Peterson " 31 | __version__ = "1.8.0" 32 | 33 | 34 | # Useful for very coarse version differentiation. 35 | PY2 = sys.version_info[0] == 2 36 | PY3 = sys.version_info[0] == 3 37 | 38 | if PY3: 39 | string_types = str, 40 | integer_types = int, 41 | class_types = type, 42 | text_type = str 43 | binary_type = bytes 44 | 45 | MAXSIZE = sys.maxsize 46 | else: 47 | string_types = basestring, 48 | integer_types = (int, long) 49 | class_types = (type, types.ClassType) 50 | text_type = unicode 51 | binary_type = str 52 | 53 | if sys.platform.startswith("java"): 54 | # Jython always uses 32 bits. 55 | MAXSIZE = int((1 << 31) - 1) 56 | else: 57 | # It's possible to have sizeof(long) != sizeof(Py_ssize_t). 58 | class X(object): 59 | def __len__(self): 60 | return 1 << 31 61 | try: 62 | len(X()) 63 | except OverflowError: 64 | # 32-bit 65 | MAXSIZE = int((1 << 31) - 1) 66 | else: 67 | # 64-bit 68 | MAXSIZE = int((1 << 63) - 1) 69 | del X 70 | 71 | 72 | def _add_doc(func, doc): 73 | """Add documentation to a function.""" 74 | func.__doc__ = doc 75 | 76 | 77 | def _import_module(name): 78 | """Import module, returning the module after the last dot.""" 79 | __import__(name) 80 | return sys.modules[name] 81 | 82 | 83 | class _LazyDescr(object): 84 | 85 | def __init__(self, name): 86 | self.name = name 87 | 88 | def __get__(self, obj, tp): 89 | result = self._resolve() 90 | setattr(obj, self.name, result) # Invokes __set__. 91 | # This is a bit ugly, but it avoids running this again. 92 | delattr(obj.__class__, self.name) 93 | return result 94 | 95 | 96 | class MovedModule(_LazyDescr): 97 | 98 | def __init__(self, name, old, new=None): 99 | super(MovedModule, self).__init__(name) 100 | if PY3: 101 | if new is None: 102 | new = name 103 | self.mod = new 104 | else: 105 | self.mod = old 106 | 107 | def _resolve(self): 108 | return _import_module(self.mod) 109 | 110 | def __getattr__(self, attr): 111 | _module = self._resolve() 112 | value = getattr(_module, attr) 113 | setattr(self, attr, value) 114 | return value 115 | 116 | 117 | class _LazyModule(types.ModuleType): 118 | 119 | def __init__(self, name): 120 | super(_LazyModule, self).__init__(name) 121 | self.__doc__ = self.__class__.__doc__ 122 | 123 | def __dir__(self): 124 | attrs = ["__doc__", "__name__"] 125 | attrs += [attr.name for attr in self._moved_attributes] 126 | return attrs 127 | 128 | # Subclasses should override this 129 | _moved_attributes = [] 130 | 131 | 132 | class MovedAttribute(_LazyDescr): 133 | 134 | def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): 135 | super(MovedAttribute, self).__init__(name) 136 | if PY3: 137 | if new_mod is None: 138 | new_mod = name 139 | self.mod = new_mod 140 | if new_attr is None: 141 | if old_attr is None: 142 | new_attr = name 143 | else: 144 | new_attr = old_attr 145 | self.attr = new_attr 146 | else: 147 | self.mod = old_mod 148 | if old_attr is None: 149 | old_attr = name 150 | self.attr = old_attr 151 | 152 | def _resolve(self): 153 | module = _import_module(self.mod) 154 | return getattr(module, self.attr) 155 | 156 | 157 | class _SixMetaPathImporter(object): 158 | """ 159 | A meta path importer to import six.moves and its submodules. 160 | 161 | This class implements a PEP302 finder and loader. It should be compatible 162 | with Python 2.5 and all existing versions of Python3 163 | """ 164 | def __init__(self, six_module_name): 165 | self.name = six_module_name 166 | self.known_modules = {} 167 | 168 | def _add_module(self, mod, *fullnames): 169 | for fullname in fullnames: 170 | self.known_modules[self.name + "." + fullname] = mod 171 | 172 | def _get_module(self, fullname): 173 | return self.known_modules[self.name + "." + fullname] 174 | 175 | def find_module(self, fullname, path=None): 176 | if fullname in self.known_modules: 177 | return self 178 | return None 179 | 180 | def __get_module(self, fullname): 181 | try: 182 | return self.known_modules[fullname] 183 | except KeyError: 184 | raise ImportError("This loader does not know module " + fullname) 185 | 186 | def load_module(self, fullname): 187 | try: 188 | # in case of a reload 189 | return sys.modules[fullname] 190 | except KeyError: 191 | pass 192 | mod = self.__get_module(fullname) 193 | if isinstance(mod, MovedModule): 194 | mod = mod._resolve() 195 | else: 196 | mod.__loader__ = self 197 | sys.modules[fullname] = mod 198 | return mod 199 | 200 | def is_package(self, fullname): 201 | """ 202 | Return true, if the named module is a package. 203 | 204 | We need this method to get correct spec objects with 205 | Python 3.4 (see PEP451) 206 | """ 207 | return hasattr(self.__get_module(fullname), "__path__") 208 | 209 | def get_code(self, fullname): 210 | """Return None 211 | 212 | Required, if is_package is implemented""" 213 | self.__get_module(fullname) # eventually raises ImportError 214 | return None 215 | get_source = get_code # same as get_code 216 | 217 | _importer = _SixMetaPathImporter(__name__) 218 | 219 | 220 | class _MovedItems(_LazyModule): 221 | """Lazy loading of moved objects""" 222 | __path__ = [] # mark as package 223 | 224 | 225 | _moved_attributes = [ 226 | MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), 227 | MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), 228 | MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), 229 | MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), 230 | MovedAttribute("intern", "__builtin__", "sys"), 231 | MovedAttribute("map", "itertools", "builtins", "imap", "map"), 232 | MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), 233 | MovedAttribute("reload_module", "__builtin__", "imp", "reload"), 234 | MovedAttribute("reduce", "__builtin__", "functools"), 235 | MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), 236 | MovedAttribute("StringIO", "StringIO", "io"), 237 | MovedAttribute("UserDict", "UserDict", "collections"), 238 | MovedAttribute("UserList", "UserList", "collections"), 239 | MovedAttribute("UserString", "UserString", "collections"), 240 | MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), 241 | MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), 242 | MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), 243 | 244 | MovedModule("builtins", "__builtin__"), 245 | MovedModule("configparser", "ConfigParser"), 246 | MovedModule("copyreg", "copy_reg"), 247 | MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), 248 | MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), 249 | MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), 250 | MovedModule("http_cookies", "Cookie", "http.cookies"), 251 | MovedModule("html_entities", "htmlentitydefs", "html.entities"), 252 | MovedModule("html_parser", "HTMLParser", "html.parser"), 253 | MovedModule("http_client", "httplib", "http.client"), 254 | MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), 255 | MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), 256 | MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), 257 | MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), 258 | MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), 259 | MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), 260 | MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), 261 | MovedModule("cPickle", "cPickle", "pickle"), 262 | MovedModule("queue", "Queue"), 263 | MovedModule("reprlib", "repr"), 264 | MovedModule("socketserver", "SocketServer"), 265 | MovedModule("_thread", "thread", "_thread"), 266 | MovedModule("tkinter", "Tkinter"), 267 | MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), 268 | MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), 269 | MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), 270 | MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), 271 | MovedModule("tkinter_tix", "Tix", "tkinter.tix"), 272 | MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), 273 | MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), 274 | MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), 275 | MovedModule("tkinter_colorchooser", "tkColorChooser", 276 | "tkinter.colorchooser"), 277 | MovedModule("tkinter_commondialog", "tkCommonDialog", 278 | "tkinter.commondialog"), 279 | MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), 280 | MovedModule("tkinter_font", "tkFont", "tkinter.font"), 281 | MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), 282 | MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", 283 | "tkinter.simpledialog"), 284 | MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), 285 | MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), 286 | MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), 287 | MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), 288 | MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), 289 | MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), 290 | MovedModule("winreg", "_winreg"), 291 | ] 292 | for attr in _moved_attributes: 293 | setattr(_MovedItems, attr.name, attr) 294 | if isinstance(attr, MovedModule): 295 | _importer._add_module(attr, "moves." + attr.name) 296 | del attr 297 | 298 | _MovedItems._moved_attributes = _moved_attributes 299 | 300 | moves = _MovedItems(__name__ + ".moves") 301 | _importer._add_module(moves, "moves") 302 | 303 | 304 | class Module_six_moves_urllib_parse(_LazyModule): 305 | """Lazy loading of moved objects in six.moves.urllib_parse""" 306 | 307 | 308 | _urllib_parse_moved_attributes = [ 309 | MovedAttribute("ParseResult", "urlparse", "urllib.parse"), 310 | MovedAttribute("SplitResult", "urlparse", "urllib.parse"), 311 | MovedAttribute("parse_qs", "urlparse", "urllib.parse"), 312 | MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), 313 | MovedAttribute("urldefrag", "urlparse", "urllib.parse"), 314 | MovedAttribute("urljoin", "urlparse", "urllib.parse"), 315 | MovedAttribute("urlparse", "urlparse", "urllib.parse"), 316 | MovedAttribute("urlsplit", "urlparse", "urllib.parse"), 317 | MovedAttribute("urlunparse", "urlparse", "urllib.parse"), 318 | MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), 319 | MovedAttribute("quote", "urllib", "urllib.parse"), 320 | MovedAttribute("quote_plus", "urllib", "urllib.parse"), 321 | MovedAttribute("unquote", "urllib", "urllib.parse"), 322 | MovedAttribute("unquote_plus", "urllib", "urllib.parse"), 323 | MovedAttribute("urlencode", "urllib", "urllib.parse"), 324 | MovedAttribute("splitquery", "urllib", "urllib.parse"), 325 | MovedAttribute("splittag", "urllib", "urllib.parse"), 326 | MovedAttribute("splituser", "urllib", "urllib.parse"), 327 | MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), 328 | MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), 329 | MovedAttribute("uses_params", "urlparse", "urllib.parse"), 330 | MovedAttribute("uses_query", "urlparse", "urllib.parse"), 331 | MovedAttribute("uses_relative", "urlparse", "urllib.parse"), 332 | ] 333 | for attr in _urllib_parse_moved_attributes: 334 | setattr(Module_six_moves_urllib_parse, attr.name, attr) 335 | del attr 336 | 337 | Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes 338 | 339 | _importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), 340 | "moves.urllib_parse", "moves.urllib.parse") 341 | 342 | 343 | class Module_six_moves_urllib_error(_LazyModule): 344 | """Lazy loading of moved objects in six.moves.urllib_error""" 345 | 346 | 347 | _urllib_error_moved_attributes = [ 348 | MovedAttribute("URLError", "urllib2", "urllib.error"), 349 | MovedAttribute("HTTPError", "urllib2", "urllib.error"), 350 | MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), 351 | ] 352 | for attr in _urllib_error_moved_attributes: 353 | setattr(Module_six_moves_urllib_error, attr.name, attr) 354 | del attr 355 | 356 | Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes 357 | 358 | _importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), 359 | "moves.urllib_error", "moves.urllib.error") 360 | 361 | 362 | class Module_six_moves_urllib_request(_LazyModule): 363 | """Lazy loading of moved objects in six.moves.urllib_request""" 364 | 365 | 366 | _urllib_request_moved_attributes = [ 367 | MovedAttribute("urlopen", "urllib2", "urllib.request"), 368 | MovedAttribute("install_opener", "urllib2", "urllib.request"), 369 | MovedAttribute("build_opener", "urllib2", "urllib.request"), 370 | MovedAttribute("pathname2url", "urllib", "urllib.request"), 371 | MovedAttribute("url2pathname", "urllib", "urllib.request"), 372 | MovedAttribute("getproxies", "urllib", "urllib.request"), 373 | MovedAttribute("Request", "urllib2", "urllib.request"), 374 | MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), 375 | MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), 376 | MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), 377 | MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), 378 | MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), 379 | MovedAttribute("BaseHandler", "urllib2", "urllib.request"), 380 | MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), 381 | MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), 382 | MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), 383 | MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), 384 | MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), 385 | MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), 386 | MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), 387 | MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), 388 | MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), 389 | MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), 390 | MovedAttribute("FileHandler", "urllib2", "urllib.request"), 391 | MovedAttribute("FTPHandler", "urllib2", "urllib.request"), 392 | MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), 393 | MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), 394 | MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), 395 | MovedAttribute("urlretrieve", "urllib", "urllib.request"), 396 | MovedAttribute("urlcleanup", "urllib", "urllib.request"), 397 | MovedAttribute("URLopener", "urllib", "urllib.request"), 398 | MovedAttribute("FancyURLopener", "urllib", "urllib.request"), 399 | MovedAttribute("proxy_bypass", "urllib", "urllib.request"), 400 | ] 401 | for attr in _urllib_request_moved_attributes: 402 | setattr(Module_six_moves_urllib_request, attr.name, attr) 403 | del attr 404 | 405 | Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes 406 | 407 | _importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), 408 | "moves.urllib_request", "moves.urllib.request") 409 | 410 | 411 | class Module_six_moves_urllib_response(_LazyModule): 412 | """Lazy loading of moved objects in six.moves.urllib_response""" 413 | 414 | 415 | _urllib_response_moved_attributes = [ 416 | MovedAttribute("addbase", "urllib", "urllib.response"), 417 | MovedAttribute("addclosehook", "urllib", "urllib.response"), 418 | MovedAttribute("addinfo", "urllib", "urllib.response"), 419 | MovedAttribute("addinfourl", "urllib", "urllib.response"), 420 | ] 421 | for attr in _urllib_response_moved_attributes: 422 | setattr(Module_six_moves_urllib_response, attr.name, attr) 423 | del attr 424 | 425 | Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes 426 | 427 | _importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), 428 | "moves.urllib_response", "moves.urllib.response") 429 | 430 | 431 | class Module_six_moves_urllib_robotparser(_LazyModule): 432 | """Lazy loading of moved objects in six.moves.urllib_robotparser""" 433 | 434 | 435 | _urllib_robotparser_moved_attributes = [ 436 | MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), 437 | ] 438 | for attr in _urllib_robotparser_moved_attributes: 439 | setattr(Module_six_moves_urllib_robotparser, attr.name, attr) 440 | del attr 441 | 442 | Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes 443 | 444 | _importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), 445 | "moves.urllib_robotparser", "moves.urllib.robotparser") 446 | 447 | 448 | class Module_six_moves_urllib(types.ModuleType): 449 | """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" 450 | __path__ = [] # mark as package 451 | parse = _importer._get_module("moves.urllib_parse") 452 | error = _importer._get_module("moves.urllib_error") 453 | request = _importer._get_module("moves.urllib_request") 454 | response = _importer._get_module("moves.urllib_response") 455 | robotparser = _importer._get_module("moves.urllib_robotparser") 456 | 457 | def __dir__(self): 458 | return ['parse', 'error', 'request', 'response', 'robotparser'] 459 | 460 | _importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), 461 | "moves.urllib") 462 | 463 | 464 | def add_move(move): 465 | """Add an item to six.moves.""" 466 | setattr(_MovedItems, move.name, move) 467 | 468 | 469 | def remove_move(name): 470 | """Remove item from six.moves.""" 471 | try: 472 | delattr(_MovedItems, name) 473 | except AttributeError: 474 | try: 475 | del moves.__dict__[name] 476 | except KeyError: 477 | raise AttributeError("no such move, %r" % (name,)) 478 | 479 | 480 | if PY3: 481 | _meth_func = "__func__" 482 | _meth_self = "__self__" 483 | 484 | _func_closure = "__closure__" 485 | _func_code = "__code__" 486 | _func_defaults = "__defaults__" 487 | _func_globals = "__globals__" 488 | else: 489 | _meth_func = "im_func" 490 | _meth_self = "im_self" 491 | 492 | _func_closure = "func_closure" 493 | _func_code = "func_code" 494 | _func_defaults = "func_defaults" 495 | _func_globals = "func_globals" 496 | 497 | 498 | try: 499 | advance_iterator = next 500 | except NameError: 501 | def advance_iterator(it): 502 | return it.next() 503 | next = advance_iterator 504 | 505 | 506 | try: 507 | callable = callable 508 | except NameError: 509 | def callable(obj): 510 | return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) 511 | 512 | 513 | if PY3: 514 | def get_unbound_function(unbound): 515 | return unbound 516 | 517 | create_bound_method = types.MethodType 518 | 519 | Iterator = object 520 | else: 521 | def get_unbound_function(unbound): 522 | return unbound.im_func 523 | 524 | def create_bound_method(func, obj): 525 | return types.MethodType(func, obj, obj.__class__) 526 | 527 | class Iterator(object): 528 | 529 | def next(self): 530 | return type(self).__next__(self) 531 | 532 | callable = callable 533 | _add_doc(get_unbound_function, 534 | """Get the function out of a possibly unbound function""") 535 | 536 | 537 | get_method_function = operator.attrgetter(_meth_func) 538 | get_method_self = operator.attrgetter(_meth_self) 539 | get_function_closure = operator.attrgetter(_func_closure) 540 | get_function_code = operator.attrgetter(_func_code) 541 | get_function_defaults = operator.attrgetter(_func_defaults) 542 | get_function_globals = operator.attrgetter(_func_globals) 543 | 544 | 545 | if PY3: 546 | def iterkeys(d, **kw): 547 | return iter(d.keys(**kw)) 548 | 549 | def itervalues(d, **kw): 550 | return iter(d.values(**kw)) 551 | 552 | def iteritems(d, **kw): 553 | return iter(d.items(**kw)) 554 | 555 | def iterlists(d, **kw): 556 | return iter(d.lists(**kw)) 557 | else: 558 | def iterkeys(d, **kw): 559 | return iter(d.iterkeys(**kw)) 560 | 561 | def itervalues(d, **kw): 562 | return iter(d.itervalues(**kw)) 563 | 564 | def iteritems(d, **kw): 565 | return iter(d.iteritems(**kw)) 566 | 567 | def iterlists(d, **kw): 568 | return iter(d.iterlists(**kw)) 569 | 570 | _add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") 571 | _add_doc(itervalues, "Return an iterator over the values of a dictionary.") 572 | _add_doc(iteritems, 573 | "Return an iterator over the (key, value) pairs of a dictionary.") 574 | _add_doc(iterlists, 575 | "Return an iterator over the (key, [values]) pairs of a dictionary.") 576 | 577 | 578 | if PY3: 579 | def b(s): 580 | return s.encode("latin-1") 581 | def u(s): 582 | return s 583 | unichr = chr 584 | if sys.version_info[1] <= 1: 585 | def int2byte(i): 586 | return bytes((i,)) 587 | else: 588 | # This is about 2x faster than the implementation above on 3.2+ 589 | int2byte = operator.methodcaller("to_bytes", 1, "big") 590 | byte2int = operator.itemgetter(0) 591 | indexbytes = operator.getitem 592 | iterbytes = iter 593 | import io 594 | StringIO = io.StringIO 595 | BytesIO = io.BytesIO 596 | else: 597 | def b(s): 598 | return s 599 | # Workaround for standalone backslash 600 | def u(s): 601 | return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") 602 | unichr = unichr 603 | int2byte = chr 604 | def byte2int(bs): 605 | return ord(bs[0]) 606 | def indexbytes(buf, i): 607 | return ord(buf[i]) 608 | def iterbytes(buf): 609 | return (ord(byte) for byte in buf) 610 | import StringIO 611 | StringIO = BytesIO = StringIO.StringIO 612 | _add_doc(b, """Byte literal""") 613 | _add_doc(u, """Text literal""") 614 | 615 | 616 | if PY3: 617 | exec_ = getattr(moves.builtins, "exec") 618 | 619 | 620 | def reraise(tp, value, tb=None): 621 | if value is None: 622 | value = tp() 623 | if value.__traceback__ is not tb: 624 | raise value.with_traceback(tb) 625 | raise value 626 | 627 | else: 628 | def exec_(_code_, _globs_=None, _locs_=None): 629 | """Execute code in a namespace.""" 630 | if _globs_ is None: 631 | frame = sys._getframe(1) 632 | _globs_ = frame.f_globals 633 | if _locs_ is None: 634 | _locs_ = frame.f_locals 635 | del frame 636 | elif _locs_ is None: 637 | _locs_ = _globs_ 638 | exec("""exec _code_ in _globs_, _locs_""") 639 | 640 | 641 | exec_("""def reraise(tp, value, tb=None): 642 | raise tp, value, tb 643 | """) 644 | 645 | 646 | print_ = getattr(moves.builtins, "print", None) 647 | if print_ is None: 648 | def print_(*args, **kwargs): 649 | """The new-style print function for Python 2.4 and 2.5.""" 650 | fp = kwargs.pop("file", sys.stdout) 651 | if fp is None: 652 | return 653 | def write(data): 654 | if not isinstance(data, basestring): 655 | data = str(data) 656 | # If the file has an encoding, encode unicode with it. 657 | if (isinstance(fp, file) and 658 | isinstance(data, unicode) and 659 | fp.encoding is not None): 660 | errors = getattr(fp, "errors", None) 661 | if errors is None: 662 | errors = "strict" 663 | data = data.encode(fp.encoding, errors) 664 | fp.write(data) 665 | want_unicode = False 666 | sep = kwargs.pop("sep", None) 667 | if sep is not None: 668 | if isinstance(sep, unicode): 669 | want_unicode = True 670 | elif not isinstance(sep, str): 671 | raise TypeError("sep must be None or a string") 672 | end = kwargs.pop("end", None) 673 | if end is not None: 674 | if isinstance(end, unicode): 675 | want_unicode = True 676 | elif not isinstance(end, str): 677 | raise TypeError("end must be None or a string") 678 | if kwargs: 679 | raise TypeError("invalid keyword arguments to print()") 680 | if not want_unicode: 681 | for arg in args: 682 | if isinstance(arg, unicode): 683 | want_unicode = True 684 | break 685 | if want_unicode: 686 | newline = unicode("\n") 687 | space = unicode(" ") 688 | else: 689 | newline = "\n" 690 | space = " " 691 | if sep is None: 692 | sep = space 693 | if end is None: 694 | end = newline 695 | for i, arg in enumerate(args): 696 | if i: 697 | write(sep) 698 | write(arg) 699 | write(end) 700 | 701 | _add_doc(reraise, """Reraise an exception.""") 702 | 703 | if sys.version_info[0:2] < (3, 4): 704 | def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, 705 | updated=functools.WRAPPER_UPDATES): 706 | def wrapper(f): 707 | f = functools.wraps(wrapped)(f) 708 | f.__wrapped__ = wrapped 709 | return f 710 | return wrapper 711 | else: 712 | wraps = functools.wraps 713 | 714 | def with_metaclass(meta, *bases): 715 | """Create a base class with a metaclass.""" 716 | # This requires a bit of explanation: the basic idea is to make a dummy 717 | # metaclass for one level of class instantiation that replaces itself with 718 | # the actual metaclass. 719 | class metaclass(meta): 720 | def __new__(cls, name, this_bases, d): 721 | return meta(name, bases, d) 722 | return type.__new__(metaclass, 'temporary_class', (), {}) 723 | 724 | 725 | def add_metaclass(metaclass): 726 | """Class decorator for creating a class with a metaclass.""" 727 | def wrapper(cls): 728 | orig_vars = cls.__dict__.copy() 729 | slots = orig_vars.get('__slots__') 730 | if slots is not None: 731 | if isinstance(slots, str): 732 | slots = [slots] 733 | for slots_var in slots: 734 | orig_vars.pop(slots_var) 735 | orig_vars.pop('__dict__', None) 736 | orig_vars.pop('__weakref__', None) 737 | return metaclass(cls.__name__, cls.__bases__, orig_vars) 738 | return wrapper 739 | 740 | # Complete the moves implementation. 741 | # This code is at the end of this module to speed up module loading. 742 | # Turn this module into a package. 743 | __path__ = [] # required for PEP 302 and PEP 451 744 | __package__ = __name__ # see PEP 366 @ReservedAssignment 745 | if globals().get("__spec__") is not None: 746 | __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable 747 | # Remove other six meta path importers, since they cause problems. This can 748 | # happen if six is removed from sys.modules and then reloaded. (Setuptools does 749 | # this for some reason.) 750 | if sys.meta_path: 751 | for i, importer in enumerate(sys.meta_path): 752 | # Here's some real nastiness: Another "instance" of the six module might 753 | # be floating around. Therefore, we can't use isinstance() to check for 754 | # the six meta path importer, since the other six instance will have 755 | # inserted an importer with different class. 756 | if (type(importer).__name__ == "_SixMetaPathImporter" and 757 | importer.name == __name__): 758 | del sys.meta_path[i] 759 | break 760 | del i, importer 761 | # Finally, add the importer to the meta path import hook. 762 | sys.meta_path.append(_importer) 763 | -------------------------------------------------------------------------------- /vimswitch/test/BaseTestCase.py: -------------------------------------------------------------------------------- 1 | import re 2 | import textwrap 3 | import unittest 4 | 5 | 6 | class BaseTestCase(unittest.TestCase): 7 | def __init__(self, *args, **kwargs): 8 | unittest.TestCase.__init__(self, *args, **kwargs) 9 | 10 | # Python < 3.2 does not have assertNotRegex 11 | if not hasattr(self, 'assertNotRegex'): 12 | self.assertNotRegex = self.assertNotRegexpMatches 13 | 14 | def assertMultilineRegexpMatches(self, string, regexp): 15 | """ 16 | Asserts that the entirety of `string` matches `regexp`. `regexp` can be 17 | a multiline string with indentation. 18 | """ 19 | regexp = textwrap.dedent(regexp) 20 | regexp = regexp.strip() 21 | regexp = '^' + regexp + '$' 22 | regexp = re.compile(regexp, re.DOTALL) 23 | 24 | self.assertRegexpMatches(string, regexp) 25 | 26 | def assertStdout(self, stdout, regexp): 27 | "Asserts that the `stdout` io stream matches `regexp`" 28 | stdoutText = stdout.getvalue().strip() 29 | self.assertMultilineRegexpMatches(stdoutText, regexp) 30 | 31 | def resetStdout(self, stdout): 32 | stdout.seek(0) 33 | stdout.truncate(0) 34 | -------------------------------------------------------------------------------- /vimswitch/test/CommonDiskIoTests.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | 4 | class CommonDiskIoTests: 5 | def setUp(self): 6 | 'Create self.diskIo here' 7 | pass 8 | 9 | def getTestPath(self, path): 10 | 'Override this if you need to prefix paths with a working directory' 11 | return os.path.normpath(path) 12 | 13 | # DiskIo.createFile 14 | 15 | def test_createFile_hasCorrectContent(self): 16 | filePath = self.getTestPath('file1.txt') 17 | 18 | self.diskIo.createFile(filePath, 'test data') 19 | 20 | actual = self.diskIo.getFileContents(filePath) 21 | expected = 'test data' 22 | self.assertEqual(actual, expected) 23 | 24 | def test_createFile_withParentDir_hasCorrectContent(self): 25 | dirPath = self.getTestPath('dir1') 26 | filePath = self.getTestPath('dir1/file1.txt') 27 | self.diskIo.createDir(dirPath) 28 | 29 | self.diskIo.createFile(filePath, 'test data') 30 | 31 | actual = self.diskIo.getFileContents(filePath) 32 | expected = 'test data' 33 | self.assertEqual(actual, expected) 34 | 35 | def test_createFile_nonExistantParentDir_raisesError(self): 36 | filePath = self.getTestPath('non_existant/file1.txt') 37 | 38 | self.assertRaises(Exception, self.diskIo.createFile, filePath, 'test data') 39 | 40 | # DiskIo.getFileContents 41 | 42 | def test_getFileContents_returnsCorrectContent(self): 43 | filePath = self.getTestPath('file1.txt') 44 | self.diskIo.createFile(filePath, 'test data') 45 | 46 | actual = self.diskIo.getFileContents(filePath) 47 | expected = 'test data' 48 | self.assertEqual(actual, expected) 49 | 50 | def test_getFileContents_nonExistantFile_raisesError(self): 51 | filePath = self.getTestPath('file1.txt') 52 | 53 | self.assertRaises(Exception, self.diskIo.getFileContents, filePath) 54 | 55 | def test_getFileContents_onDir_raisesError(self): 56 | dirPath = self.getTestPath('dir1') 57 | self.diskIo.createDir(dirPath) 58 | 59 | self.assertRaises(Exception, self.diskIo.getFileContents, dirPath) 60 | 61 | # DiskIo.copyFile 62 | 63 | def test_copyFile_copiesFile(self): 64 | filePath = self.getTestPath('file1.txt') 65 | copiedFilePath = self.getTestPath('copied_file1.txt') 66 | self.diskIo.createFile(filePath, 'test data') 67 | 68 | self.diskIo.copyFile(filePath, copiedFilePath) 69 | 70 | actual = self.diskIo.getFileContents(copiedFilePath) 71 | expected = 'test data' 72 | self.assertEqual(actual, expected) 73 | 74 | def test_copyFile_copiesIntoDir(self): 75 | filePath = self.getTestPath('file1.txt') 76 | dirPath = self.getTestPath('dir1') 77 | copiedFilePath = self.getTestPath('dir1/file1.txt') 78 | self.diskIo.createFile(filePath, 'test data') 79 | self.diskIo.createDir(dirPath) 80 | 81 | self.diskIo.copyFile(filePath, dirPath) 82 | 83 | actual = self.diskIo.getFileContents(copiedFilePath) 84 | expected = 'test data' 85 | self.assertEqual(actual, expected) 86 | 87 | def test_copyFile_overwritesExisting(self): 88 | filePath = self.getTestPath('file1.txt') 89 | existingFilePath = self.getTestPath('existing_file1.txt') 90 | self.diskIo.createFile(filePath, 'test data') 91 | self.diskIo.createFile(existingFilePath, 'existing data') 92 | 93 | self.diskIo.copyFile(filePath, existingFilePath) 94 | 95 | actual = self.diskIo.getFileContents(existingFilePath) 96 | expected = 'test data' 97 | self.assertEqual(actual, expected) 98 | 99 | def test_copyFile_sourceFileDoesNotExist_raisesError(self): 100 | filePath = self.getTestPath('file1.txt') 101 | copiedFilePath = self.getTestPath('copied_file1.txt') 102 | 103 | self.assertRaises(Exception, self.diskIo.copyFile, filePath, copiedFilePath) 104 | 105 | # DiskIo.move 106 | 107 | def test_move_movesFile(self): 108 | filePath = self.getTestPath('file1.txt') 109 | movedFilePath = self.getTestPath('moved_file1.txt') 110 | self.diskIo.createFile(filePath, 'test data') 111 | 112 | self.diskIo.move(filePath, movedFilePath) 113 | 114 | self.assertFalse(self.diskIo.anyExists(filePath)) 115 | actual = self.diskIo.getFileContents(movedFilePath) 116 | expected = 'test data' 117 | self.assertEqual(actual, expected) 118 | 119 | def test_move_overwritesExisting(self): 120 | filePath = self.getTestPath('file1.txt') 121 | existingFilePath = self.getTestPath('existing.txt') 122 | self.diskIo.createFile(filePath, 'test data') 123 | self.diskIo.createFile(existingFilePath, 'existing data') 124 | 125 | self.diskIo.move(filePath, existingFilePath) 126 | 127 | self.assertFalse(self.diskIo.anyExists(filePath)) 128 | actual = self.diskIo.getFileContents(existingFilePath) 129 | expected = 'test data' 130 | self.assertEqual(actual, expected) 131 | 132 | def test_move_movesIntoExistingDir(self): 133 | filePath = self.getTestPath('file1.txt') 134 | existingDir = self.getTestPath('existing') 135 | movedFilePath = self.getTestPath('existing/file1.txt') 136 | self.diskIo.createFile(filePath, 'test data') 137 | self.diskIo.createDir(existingDir) 138 | 139 | self.diskIo.move(filePath, movedFilePath) 140 | 141 | self.assertFalse(self.diskIo.anyExists(filePath)) 142 | actual = self.diskIo.getFileContents(movedFilePath) 143 | expected = 'test data' 144 | self.assertEqual(actual, expected) 145 | 146 | def test_move_intoNonExistingDir_raisesError(self): 147 | filePath = self.getTestPath('file1.txt') 148 | movedFilePath = self.getTestPath('non_existing/file1.txt') 149 | self.diskIo.createFile(filePath, 'test data') 150 | 151 | self.assertRaises(Exception, self.diskIo.move, filePath, movedFilePath) 152 | 153 | # DiskIo.deleteFile 154 | 155 | def test_deleteFile_deletesFile(self): 156 | filePath = self.getTestPath('file1.txt') 157 | self.diskIo.createFile(filePath, 'test data') 158 | 159 | self.diskIo.deleteFile(filePath) 160 | 161 | self.assertFalse(self.diskIo.anyExists(filePath)) 162 | 163 | def test_deleteFile_fileDoesNotExist_raisesError(self): 164 | filePath = self.getTestPath('file1.txt') 165 | self.assertRaises(Exception, self.diskIo.deleteFile, filePath) 166 | 167 | def test_deleteFile_fileIsDir_raisesError(self): 168 | dirPath = self.getTestPath('dir1') 169 | self.diskIo.createDir(dirPath) 170 | self.assertRaises(Exception, self.diskIo.deleteFile, dirPath) 171 | 172 | # DiskIo.fileExists 173 | 174 | def test_fileExists_fileDoesExist_returnsTrue(self): 175 | filePath = self.getTestPath('file1.txt') 176 | self.diskIo.createFile(filePath, 'test data') 177 | 178 | self.assertTrue(self.diskIo.fileExists(filePath)) 179 | 180 | def test_fileExists_fileDoesNotExist_returnsFalse(self): 181 | filePath = self.getTestPath('file1.txt') 182 | self.assertFalse(self.diskIo.fileExists(filePath)) 183 | 184 | def test_fileExists_dirExists_returnsFalse(self): 185 | dirPath = self.getTestPath('dir1') 186 | self.diskIo.createDir(dirPath) 187 | self.assertFalse(self.diskIo.fileExists(dirPath)) 188 | 189 | # DiskIo.createDir 190 | 191 | def test_createDir_createsDir(self): 192 | dirPath = self.getTestPath('dir1') 193 | self.diskIo.createDir(dirPath) 194 | self.assertTrue(self.diskIo.dirExists(dirPath)) 195 | 196 | def test_createDir_withParent_createsDir(self): 197 | parentDirPath = self.getTestPath('parent') 198 | childDirPath = self.getTestPath('parent/child') 199 | 200 | self.diskIo.createDir(parentDirPath) 201 | self.diskIo.createDir(childDirPath) 202 | 203 | self.assertTrue(self.diskIo.dirExists(childDirPath)) 204 | 205 | def test_createDir_withoutParent_raisesError(self): 206 | childDirPath = self.getTestPath('parent/child') 207 | self.assertRaises(Exception, self.diskIo.createDir, childDirPath) 208 | 209 | def test_createDir_dirAlreadyExists_raisesError(self): 210 | dirPath = self.getTestPath('dir1') 211 | self.diskIo.createDir(dirPath) 212 | self.assertRaises(Exception, self.diskIo.createDir, dirPath) 213 | 214 | # DiskIo.createDirWithParents 215 | 216 | def test_createDirWithParents_createsDir(self): 217 | dirPath = self.getTestPath('dir1') 218 | self.diskIo.createDirWithParents(dirPath) 219 | self.assertTrue(self.diskIo.dirExists(dirPath)) 220 | 221 | def test_createDirWithParents_withParent_createsDir(self): 222 | parentDirPath = self.getTestPath('parent') 223 | childDirPath = self.getTestPath('parent/child') 224 | 225 | self.diskIo.createDirWithParents(parentDirPath) 226 | self.diskIo.createDirWithParents(childDirPath) 227 | 228 | self.assertTrue(self.diskIo.dirExists(childDirPath)) 229 | 230 | def test_createDirWithParents_withoutParent_createsDir(self): 231 | childDirPath = self.getTestPath('parent/child') 232 | self.diskIo.createDirWithParents(childDirPath) 233 | self.assertTrue(self.diskIo.dirExists(childDirPath)) 234 | 235 | def test_createDirWithParents_dirAlreadyExists_raisesError(self): 236 | dirPath = self.getTestPath('dir1') 237 | self.diskIo.createDirWithParents(dirPath) 238 | self.assertRaises(Exception, self.diskIo.createDirWithParents, dirPath) 239 | 240 | # DiskIo.copyDir 241 | 242 | def test_copyDir_copiesEmptyDir(self): 243 | dirPath = self.getTestPath('dir1') 244 | copiedDirPath = self.getTestPath('copiedDir') 245 | self.diskIo.createDir(dirPath) 246 | 247 | self.diskIo.copyDir(dirPath, copiedDirPath) 248 | 249 | self.assertTrue(self.diskIo.dirExists(copiedDirPath)) 250 | 251 | def test_copyDir_copiesRecursively(self): 252 | dirPath = self.getTestPath('dir1') 253 | innerDirPath = self.getTestPath('dir1/innerDir') 254 | innerFilePath = self.getTestPath('dir1/innerFile.txt') 255 | copiedDirPath = self.getTestPath('copiedDir') 256 | copiedInnerDirPath = self.getTestPath('copiedDir/innerDir') 257 | copiedInnerFilePath = self.getTestPath('copiedDir/innerFile.txt') 258 | self.diskIo.createDir(dirPath) 259 | self.diskIo.createDir(innerDirPath) 260 | self.diskIo.createFile(innerFilePath, 'test data') 261 | 262 | self.diskIo.copyDir(dirPath, copiedDirPath) 263 | 264 | self.assertTrue(self.diskIo.dirExists(copiedDirPath)) 265 | self.assertTrue(self.diskIo.dirExists(copiedInnerDirPath)) 266 | self.assertTrue(self.diskIo.fileExists(copiedInnerFilePath)) 267 | self.assertEqual(self.diskIo.getFileContents(copiedInnerFilePath), 'test data') 268 | 269 | def test_copyDir_destParentDirDoesNotExist_createsParentDir(self): 270 | dirPath = self.getTestPath('dir1') 271 | copiedParentDirPath = self.getTestPath('non_existant') 272 | copiedDirPath = self.getTestPath('non_existant/copiedDir') 273 | self.diskIo.createDir(dirPath) 274 | 275 | self.diskIo.copyDir(dirPath, copiedDirPath) 276 | 277 | self.assertTrue(self.diskIo.dirExists(copiedParentDirPath)) 278 | self.assertTrue(self.diskIo.dirExists(copiedDirPath)) 279 | 280 | def test_copyDir_destDirExists_raisesError(self): 281 | dirPath = self.getTestPath('dir1') 282 | existingDirPath = self.getTestPath('existing') 283 | self.diskIo.createDir(dirPath) 284 | self.diskIo.createDir(existingDirPath) 285 | 286 | self.assertRaises(Exception, self.diskIo.copyDir, dirPath, existingDirPath) 287 | 288 | def test_copyDir_sourceDirDoesNotExist_raisesError(self): 289 | nonExistantDirPath = self.getTestPath('non_existant') 290 | copiedDirPath = self.getTestPath('copiedDir') 291 | self.assertRaises(Exception, self.diskIo.copyDir, nonExistantDirPath, copiedDirPath) 292 | 293 | # DiskIo.deleteDir 294 | 295 | def test_deleteDir_deletesEmptyDir(self): 296 | dirPath = self.getTestPath('dir1') 297 | self.diskIo.createDir(dirPath) 298 | 299 | self.diskIo.deleteDir(dirPath) 300 | 301 | self.assertFalse(self.diskIo.anyExists(dirPath)) 302 | 303 | def test_deleteDir_dirContainsFiles_deletesDirAndContents(self): 304 | dirPath = self.getTestPath('dir1') 305 | innerDirPath = self.getTestPath('dir1/innerDir') 306 | innerFilePath = self.getTestPath('dir1/innerFile.txt') 307 | self.diskIo.createDir(dirPath) 308 | self.diskIo.createDir(innerDirPath) 309 | self.diskIo.createFile(innerFilePath, 'test data') 310 | 311 | self.diskIo.deleteDir(dirPath) 312 | 313 | self.assertFalse(self.diskIo.anyExists(dirPath)) 314 | self.assertFalse(self.diskIo.anyExists(innerDirPath)) 315 | self.assertFalse(self.diskIo.anyExists(innerFilePath)) 316 | 317 | def test_deleteDir_dirDoesNotExist_raiseError(self): 318 | dirPath = self.getTestPath('dir1') 319 | self.assertRaises(Exception, self.diskIo.deleteDir, dirPath) 320 | 321 | def test_deleteDir_deletesReadOnlyFiles(self): 322 | dirPath = self.getTestPath('dir1') 323 | innerDirPath = self.getTestPath('dir1/innerDir') 324 | innerFilePath = self.getTestPath('dir1/innerFile.txt') 325 | self.diskIo.createDir(dirPath) 326 | self.diskIo.createDir(innerDirPath) 327 | self.diskIo.createFile(innerFilePath, 'test data') 328 | self.diskIo.setReadOnly(innerDirPath, True) 329 | self.diskIo.setReadOnly(innerFilePath, True) 330 | 331 | self.diskIo.deleteDir(dirPath) 332 | 333 | self.assertFalse(self.diskIo.anyExists(dirPath)) 334 | self.assertFalse(self.diskIo.anyExists(innerDirPath)) 335 | self.assertFalse(self.diskIo.anyExists(innerFilePath)) 336 | 337 | # DiskIo.dirExists 338 | 339 | def test_dirExists_dirDoesExist_returnsTrue(self): 340 | dirPath = self.getTestPath('dir1') 341 | self.diskIo.createDir(dirPath) 342 | self.assertTrue(self.diskIo.dirExists(dirPath)) 343 | 344 | def test_dirExists_dirDoesNotExists_returnsFalse(self): 345 | dirPath = self.getTestPath('dir1') 346 | self.assertFalse(self.diskIo.dirExists(dirPath)) 347 | 348 | def test_dirExists_fileExists_returnFalse(self): 349 | filePath = self.getTestPath('file1.txt') 350 | self.diskIo.createFile(filePath, 'test data') 351 | self.assertFalse(self.diskIo.dirExists(filePath)) 352 | 353 | def test_dirExists_parentDirDoesNotExist_returnsFalse(self): 354 | childDir = self.getTestPath('parentDir/childDir') 355 | self.assertFalse(self.diskIo.dirExists(childDir)) 356 | 357 | # DiskIo.isDirEmpty 358 | 359 | def test_isDirEmpty_emptyDir_returnsTrue(self): 360 | dirPath = self.getTestPath('empty') 361 | self.diskIo.createDir(dirPath) 362 | 363 | self.assertTrue(self.diskIo.isDirEmpty(dirPath)) 364 | 365 | def test_isDirEmpty_nonEmptyDir_returnsFalse(self): 366 | dirPath = self.getTestPath('non_empty') 367 | filePath = self.getTestPath('non_empty/file1.txt') 368 | self.diskIo.createDir(dirPath) 369 | self.diskIo.createFile(filePath, 'test data') 370 | 371 | self.assertFalse(self.diskIo.isDirEmpty(dirPath)) 372 | 373 | def test_isDirEmpty_nonExistantDir_raisesError(self): 374 | dirPath = self.getTestPath('non_existant') 375 | 376 | self.assertRaises(Exception, self.diskIo.isDirEmpty, dirPath) 377 | 378 | def test_isDirEmpty_onFile_raisesError(self): 379 | filePath = self.getTestPath('file1.txt') 380 | self.diskIo.createFile(filePath, 'test data') 381 | 382 | self.assertRaises(Exception, self.diskIo.isDirEmpty, filePath) 383 | 384 | # DiskIo.getDirContents 385 | 386 | def test_getDirContents_emptyDir_returnsEmptyList(self): 387 | dirPath = self.getTestPath('dir1') 388 | self.diskIo.createDir(dirPath) 389 | 390 | dirContents = self.diskIo.getDirContents(dirPath) 391 | 392 | self.assertEqual(dirContents, []) 393 | 394 | def test_getDirContents_dirWithOneFile_returnsSingleElementList(self): 395 | dirPath = self.getTestPath('dir1') 396 | self.diskIo.createDir(dirPath) 397 | innerFilePath = self.getTestPath('dir1/innerFile.txt') 398 | self.diskIo.createFile(innerFilePath, 'test data') 399 | 400 | dirContents = self.diskIo.getDirContents(dirPath) 401 | 402 | self.assertEqual(dirContents, ['innerFile.txt']) 403 | 404 | def test_getDirContents_dirWithOneDir_returnsSingleElementList(self): 405 | dirPath = self.getTestPath('dir1') 406 | self.diskIo.createDir(dirPath) 407 | innerDirPath = self.getTestPath('dir1/innerDir') 408 | self.diskIo.createDir(innerDirPath) 409 | 410 | dirContents = self.diskIo.getDirContents(dirPath) 411 | 412 | self.assertEqual(dirContents, ['innerDir']) 413 | 414 | def test_getDirContents_dirWithManyFiles_returnsList(self): 415 | dirPath = self.getTestPath('dir1') 416 | self.diskIo.createDir(dirPath) 417 | innerFilePath = self.getTestPath('dir1/innerFile.txt') 418 | self.diskIo.createFile(innerFilePath, 'test data') 419 | innerDirPath = self.getTestPath('dir1/innerDir') 420 | self.diskIo.createDir(innerDirPath) 421 | innerFile2Path = self.getTestPath('dir1/innerDir/innerFile2.txt') 422 | self.diskIo.createFile(innerFile2Path, 'test data') 423 | 424 | dirContents = self.diskIo.getDirContents(dirPath) 425 | 426 | self.assertIn('innerFile.txt', dirContents) 427 | self.assertIn('innerDir', dirContents) 428 | self.assertEqual(len(dirContents), 2) 429 | 430 | # DiskIo.anyExists 431 | 432 | def test_anyExists_withFile_returnsTrue(self): 433 | filePath = self.getTestPath('file1.txt') 434 | self.diskIo.createFile(filePath, 'test data') 435 | self.assertTrue(self.diskIo.anyExists(filePath)) 436 | 437 | def test_anyExists_withDir_returnsTrue(self): 438 | dirPath = self.getTestPath('dir1') 439 | self.diskIo.createDir(dirPath) 440 | self.assertTrue(self.diskIo.anyExists(dirPath)) 441 | 442 | def test_anyExists_withNothing_returnsFalse(self): 443 | nonExistantPath = self.getTestPath('non_existant') 444 | self.assertFalse(self.diskIo.anyExists(nonExistantPath)) 445 | 446 | # DiskIo.setReadOnly / DiskIo.isReadOnly 447 | 448 | def test_readOnly_withReadOnlyFile_returnsTrue(self): 449 | filePath = self.getTestPath('read_only.txt') 450 | self.diskIo.createFile(filePath, 'test data') 451 | self.diskIo.setReadOnly(filePath, True) 452 | 453 | self.assertTrue(self.diskIo.isReadOnly(filePath)) 454 | 455 | def test_readOnly_withReadOnlyDir_returnsTrue(self): 456 | dirPath = self.getTestPath('read_only') 457 | self.diskIo.createDir(dirPath) 458 | self.diskIo.setReadOnly(dirPath, True) 459 | 460 | self.assertTrue(self.diskIo.isReadOnly(dirPath)) 461 | 462 | def test_readOnly_withWritableFile_returnsTrue(self): 463 | filePath = self.getTestPath('writable.txt') 464 | self.diskIo.createFile(filePath, 'test data') 465 | self.diskIo.setReadOnly(filePath, False) 466 | 467 | self.assertFalse(self.diskIo.isReadOnly(filePath)) 468 | 469 | def test_readOnly_withWritableDir_returnsTrue(self): 470 | dirPath = self.getTestPath('writable') 471 | self.diskIo.createDir(dirPath) 472 | self.diskIo.setReadOnly(dirPath, False) 473 | 474 | self.assertFalse(self.diskIo.isReadOnly(dirPath)) 475 | -------------------------------------------------------------------------------- /vimswitch/test/FakeFileDownloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | from vimswitch.DiskIo import createDiskIo 3 | 4 | 5 | class FakeFileDownloader: 6 | """ 7 | Fakes FileDownloader by downloading files from the filesystem. Download URLs 8 | are converted to a filesystem friendly format as follows: 9 | - `root` will be used as starting directory 10 | - Everything before the URL's basename will be used as the directory 11 | name 12 | - The filename will be the URL's basename 13 | - Replace '://' and '/' with '.'. 14 | 15 | For example, a call to 16 | `download('https://github.com/test/vimrc/archive/master.zip', path)` 17 | will copy the file located at 18 | `$root/https.github.com.test.vimrc.archive/master.zip` to `path` 19 | 20 | If the URL points to a non existant file it will raise an IOError. 21 | """ 22 | 23 | def __init__(self, root, diskIo): 24 | self.root = os.path.normpath(root) 25 | self.diskIo = diskIo 26 | 27 | def download(self, url, path): 28 | (directory, baseName) = os.path.split(url) 29 | directory = directory.replace('://', '.').replace('/', '.') 30 | sourcePath = os.path.join(self.root, directory, baseName) 31 | if self.diskIo.dirExists(path): 32 | destPath = os.path.join(path, baseName) 33 | else: 34 | destPath = path 35 | 36 | try: 37 | self.diskIo.copyFile(sourcePath, destPath) 38 | except IOError: 39 | message = 'Error when accessing %s: 404 File not found' % url 40 | raise IOError(message) 41 | 42 | return destPath 43 | 44 | 45 | def createFakeFileDownloader(app, root): 46 | diskIo = app.get('diskIo', createDiskIo(app)) 47 | fakeFileDownloader = FakeFileDownloader(root, diskIo) 48 | return fakeFileDownloader 49 | -------------------------------------------------------------------------------- /vimswitch/test/FileSystemSandbox.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import vimswitch.six.moves.builtins as builtins 4 | 5 | 6 | class FileSystemSandbox: 7 | """ 8 | This static class sets up a global sandbox where all disk modification is 9 | disabled except inside the sandbox directory. If an operation outside the 10 | sandbox directory occurs, a FileSystemSandboxError is thrown. 11 | 12 | Read-only disk operations will still be allowed outside the sandbox 13 | directory, however. 14 | """ 15 | 16 | enabled = False 17 | sandboxRoot = '' 18 | 19 | def enable(self, sandboxRoot): 20 | if self.enabled: 21 | raise RuntimeError('Sandbox already enabled') 22 | 23 | self.enabled = True 24 | self.sandboxRoot = sandboxRoot 25 | self._setUpSafeOperations() 26 | 27 | def disable(self): 28 | if not self.enabled: 29 | raise RuntimeError('Sandbox already disabled') 30 | 31 | self.enabled = False 32 | self._tearDownSafeOperations() 33 | 34 | def _setUpSafeOperations(self): 35 | self._real_builtin_open = builtins.open 36 | self._real_os_mkdir = os.mkdir 37 | self._real_os_makedirs = os.makedirs 38 | self._real_os_remove = os.remove 39 | self._real_os_path_isfile = os.path.isfile 40 | self._real_os_path_isdir = os.path.isdir 41 | self._real_shutil_copy = shutil.copy 42 | self._real_shutil_move = shutil.move 43 | self._real_shutil_copytree = shutil.copytree 44 | self._real_shutil_rmtree = shutil.rmtree 45 | 46 | builtins.open = self._safe_builtin_open 47 | os.mkdir = self._safe_os_mkdir 48 | os.makedirs = self._safe_os_makedirs 49 | os.remove = self._safe_os_remove 50 | shutil.copy = self._safe_shutil_copy 51 | shutil.move = self._safe_shutil_move 52 | shutil.copytree = self._safe_shutil_copytree 53 | shutil.rmtree = self._safe_shutil_rmtree 54 | 55 | def _tearDownSafeOperations(self): 56 | builtins.open = self._real_builtin_open 57 | os.mkdir = self._real_os_mkdir 58 | os.makedirs = self._real_os_makedirs 59 | os.remove = self._real_os_remove 60 | shutil.copy = self._real_shutil_copy 61 | shutil.move = self._real_shutil_move 62 | shutil.copytree = self._real_shutil_copytree 63 | shutil.rmtree = self._real_shutil_rmtree 64 | 65 | def _safe_builtin_open(self, path, mode='r', *args, **kwargs): 66 | # We only verify if the file is being opened for writing or appending. 67 | # Read only access should be allowed. 68 | if mode.find('w') != -1 or mode.find('a') != -1: 69 | self._verifyPath(path) 70 | return self._real_builtin_open(path, mode, *args, **kwargs) 71 | 72 | def _safe_os_mkdir(self, path, *args, **kwargs): 73 | self._verifyPath(path) 74 | self._real_os_mkdir(path, *args, **kwargs) 75 | 76 | def _safe_os_makedirs(self, path, *args, **kwargs): 77 | self._verifyPath(path) 78 | self._real_os_makedirs(path, *args, **kwargs) 79 | 80 | def _safe_os_remove(self, path): 81 | self._verifyPath(path) 82 | self._real_os_remove(path) 83 | 84 | def _safe_shutil_copy(self, src, dst): 85 | # Only need to verify destination path since src will not be modified 86 | self._verifyPath(dst) 87 | self._real_shutil_copy(src, dst) 88 | 89 | def _safe_shutil_move(self, src, dst): 90 | self._verifyPath(src) 91 | self._verifyPath(dst) 92 | self._real_shutil_move(src, dst) 93 | 94 | def _safe_shutil_copytree(self, src, dst, *args, **kwargs): 95 | # Only need to verify destination path since src will not be modified 96 | self._verifyPath(dst) 97 | self._real_shutil_copytree(src, dst, *args, **kwargs) 98 | 99 | def _safe_shutil_rmtree(self, path, *args, **kwargs): 100 | self._verifyPath(path) 101 | self._real_shutil_rmtree(path, *args, **kwargs) 102 | 103 | def _verifyPath(self, path): 104 | "Checks that path is inside the sandbox" 105 | absPath = os.path.abspath(path) 106 | if not absPath.startswith(self.sandboxRoot): 107 | raise FileSystemSandboxError(path) 108 | 109 | 110 | class FileSystemSandboxError(Exception): 111 | def __init__(self, path): 112 | Exception.__init__(self, 'Tried to access path outside sandbox: %s' % path) 113 | -------------------------------------------------------------------------------- /vimswitch/test/FileSystemTestCase.py: -------------------------------------------------------------------------------- 1 | from .BaseTestCase import BaseTestCase 2 | from .FileSystemSandbox import FileSystemSandbox 3 | import os 4 | import shutil 5 | import stat 6 | 7 | 8 | class FileSystemTestCase(BaseTestCase): 9 | """ 10 | Inherit from this class when a unit test needs to access the disk. This 11 | class sets up a sandbox so that disk modifications only occur within a 12 | working directory. This keeps test code from affecting other files on the 13 | system. 14 | 15 | To get a path relative to the working directory use getTestPath(). To get a 16 | path to test data use getDataPath(). 17 | 18 | The working directory is cleared after every test so there is no need to do 19 | it manually within your tests. 20 | """ 21 | 22 | def setUp(self): 23 | self.sandbox = FileSystemSandbox() 24 | self.sandbox.enable(self.getWorkingDir()) 25 | self.clearWorkingDirectory() 26 | 27 | def tearDown(self): 28 | self.clearWorkingDirectory() 29 | self.sandbox.disable() 30 | 31 | @classmethod 32 | def getMyDir(self): 33 | return os.path.dirname(__file__) 34 | 35 | @classmethod 36 | def getTestPath(self, path): 37 | "Returns path prepended by the working directory" 38 | fullPath = os.path.join(self.getWorkingDir(), path) 39 | return os.path.normpath(fullPath) 40 | 41 | @classmethod 42 | def getDataPath(self, path): 43 | "Returns path prepended by the test data directory" 44 | dataDir = 'data' 45 | fullPath = os.path.join(self.getMyDir(), dataDir, path) 46 | return os.path.normpath(fullPath) 47 | 48 | @classmethod 49 | def getWorkingDir(self): 50 | """"Returns the path to a directory where we can safely create files and 51 | directories during tests""" 52 | dirName = 'workingDir' 53 | return os.path.join(self.getMyDir(), dirName) 54 | 55 | def copyDataToWorkingDir(self, dataSrc, workingDirDest): 56 | "Copies a file or dir from the data directory to the working directory" 57 | dataSrc = self.getDataPath(dataSrc) 58 | workingDirDest = self.getTestPath(workingDirDest) 59 | if os.path.isdir(dataSrc): 60 | shutil.copytree(dataSrc, workingDirDest) 61 | else: 62 | shutil.copy(dataSrc, workingDirDest) 63 | 64 | def clearWorkingDirectory(self): 65 | for entry in os.listdir(self.getWorkingDir()): 66 | fullPath = os.path.join(self.getWorkingDir(), entry) 67 | if os.path.isfile(fullPath): 68 | # If file is readonly, make it writable 69 | if not os.access(fullPath, os.W_OK): 70 | os.chmod(fullPath, stat.S_IWRITE) 71 | os.remove(fullPath) 72 | elif os.path.isdir(fullPath): 73 | shutil.rmtree(fullPath, onerror=_remove_readonly) 74 | 75 | 76 | def _remove_readonly(func, path, excinfo): 77 | os.chmod(path, stat.S_IWRITE) 78 | func(path) 79 | -------------------------------------------------------------------------------- /vimswitch/test/SimpleServer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from vimswitch.six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler 3 | import vimswitch.six.moves.socketserver as socketserver 4 | import threading 5 | from time import sleep 6 | from vimswitch.six.moves import input 7 | 8 | 9 | class SimpleServer(threading.Thread): 10 | def __init__(self, pathToServe, address='127.0.0.1', port=0): 11 | self.pathToServe = pathToServe 12 | self.address = address 13 | self.port = port 14 | self.httpd = None 15 | threading.Thread.__init__(self) 16 | 17 | def run(self): 18 | self.httpd = socketserver.TCPServer((self.address, self.port), 19 | self.SimpleRequestHandler) 20 | self.httpd.pathToServe = self.pathToServe 21 | self.httpd.serve_forever() 22 | 23 | def getPort(self): 24 | self.waitForServer() 25 | ip, port = self.httpd.server_address 26 | return port 27 | 28 | def stop(self): 29 | self.waitForServer() 30 | self.httpd.shutdown() 31 | self.httpd.socket.close() 32 | 33 | def waitForServer(self): 34 | # Wait until self.httpd is intitialised 35 | for _ in range(1000): 36 | if self.httpd is not None: 37 | return 38 | sleep(0.001) 39 | 40 | class SimpleRequestHandler(SimpleHTTPRequestHandler): 41 | def __init__(self, *args, **kwargs): 42 | SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) 43 | 44 | def translate_path(self, path): 45 | # TODO: prevent path traversal 46 | path = path[1:] # Remove starting slash 47 | pathToServe = self.server.pathToServe 48 | #print('path (%s), pathToServe (%s)' % (path, pathToServe)) 49 | #print('Serving (%s)' % os.path.relpath(path, pathToServe)) 50 | #return os.path.relpath(path, pathToServe) 51 | return os.path.join(pathToServe, path) 52 | 53 | def log_message(self, format, *args): 54 | # Disable logging 55 | pass 56 | 57 | 58 | if __name__ == '__main__': 59 | try: 60 | s = SimpleServer(os.path.abspath('')) 61 | s.start() 62 | input('Press Enter to stop server...\n') 63 | finally: 64 | s.stop() 65 | -------------------------------------------------------------------------------- /vimswitch/test/Stubs.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | from vimswitch.DiskIo import DiskIo 3 | from vimswitch.ProfileDataIo import ProfileDataIo 4 | 5 | 6 | def DiskIoStub(): 7 | diskIo = MagicMock(DiskIo) 8 | return diskIo 9 | 10 | 11 | def ProfileDataIoStub(): 12 | profileDataIo = MagicMock(ProfileDataIo) 13 | return profileDataIo 14 | -------------------------------------------------------------------------------- /vimswitch/test/TestDiskIo.py: -------------------------------------------------------------------------------- 1 | from .FileSystemTestCase import FileSystemTestCase 2 | from .CommonDiskIoTests import CommonDiskIoTests 3 | from vimswitch.DiskIo import DiskIo 4 | 5 | 6 | class TestDiskIo(FileSystemTestCase, CommonDiskIoTests): 7 | def setUp(self): 8 | self.diskIo = DiskIo() 9 | FileSystemTestCase.setUp(self) 10 | 11 | def getTestPath(self, path): 12 | return FileSystemTestCase.getTestPath(path) 13 | -------------------------------------------------------------------------------- /vimswitch/test/TestHelpers.py: -------------------------------------------------------------------------------- 1 | def assertNoCall(mock, *args): 2 | calledWith = False 3 | try: 4 | mock.assert_any_call(*args) 5 | calledWith = True 6 | except AssertionError: 7 | pass 8 | 9 | if calledWith: 10 | raise AssertionError('Mock called with arguments', mock, args) 11 | 12 | 13 | class Contains(str): 14 | def __eq__(self, other): 15 | return self in other 16 | -------------------------------------------------------------------------------- /vimswitch/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/__init__.py -------------------------------------------------------------------------------- /vimswitch/test/data/fake_internet/README: -------------------------------------------------------------------------------- 1 | This directory is used by vimswitch.test.FakeFileDownloader to fake the internet using the filesystem. For example, if FakeFileDownloader is asked to download 'https://github.com/test/vimrc/archive/master.zip', it will copy the file located at 'fake_internet/https.github.com.test.vimrc.archive/master.zip' to the desired path. -------------------------------------------------------------------------------- /vimswitch/test/data/fake_internet/https.github.com.test.vimrc.archive/master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/data/fake_internet/https.github.com.test.vimrc.archive/master.zip -------------------------------------------------------------------------------- /vimswitch/test/data/fake_internet/https.github.com.test2.vimrc.archive/master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/data/fake_internet/https.github.com.test2.vimrc.archive/master.zip -------------------------------------------------------------------------------- /vimswitch/test/data/fake_internet2/README: -------------------------------------------------------------------------------- 1 | This directory is an alternative version of fake_internet that can be used to represent the internet at a different point in time. -------------------------------------------------------------------------------- /vimswitch/test/data/fake_internet2/https.github.com.test.vimrc.archive/master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/data/fake_internet2/https.github.com.test.vimrc.archive/master.zip -------------------------------------------------------------------------------- /vimswitch/test/data/github_zipball_multiple_root_dirs.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/data/github_zipball_multiple_root_dirs.zip -------------------------------------------------------------------------------- /vimswitch/test/data/github_zipball_no_root_dir.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/data/github_zipball_no_root_dir.zip -------------------------------------------------------------------------------- /vimswitch/test/data/home/.vim/plugin/dummy_plugin.vim: -------------------------------------------------------------------------------- 1 | " dummy home vim plugin -------------------------------------------------------------------------------- /vimswitch/test/data/home/.vimrc: -------------------------------------------------------------------------------- 1 | " home vimrc data -------------------------------------------------------------------------------- /vimswitch/test/data/simple.txt: -------------------------------------------------------------------------------- 1 | test data -------------------------------------------------------------------------------- /vimswitch/test/data/simple.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/data/simple.zip -------------------------------------------------------------------------------- /vimswitch/test/data/vimrc-master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/priomsrb/vimswitch/8d5732bbe9f0c00a8dd3e85c6ef18e37f4df49bb/vimswitch/test/data/vimrc-master.zip -------------------------------------------------------------------------------- /vimswitch/test/data/vimswitchrc: -------------------------------------------------------------------------------- 1 | [settings] 2 | currentProfile = test/vimrc -------------------------------------------------------------------------------- /vimswitch/test/test_ActionResolver.py: -------------------------------------------------------------------------------- 1 | from .BaseTestCase import BaseTestCase 2 | from .Stubs import DiskIoStub 3 | from mock import patch 4 | from vimswitch.ActionResolver import ActionResolver 5 | from vimswitch.Application import Application 6 | from vimswitch.CommandLineParser import CommandLineParser 7 | from vimswitch.Profile import Profile 8 | 9 | 10 | class TestActionResolver(BaseTestCase): 11 | 12 | def setUp(self): 13 | app = Application() 14 | app.diskIo = DiskIoStub() # Fake all disk operations 15 | self.commandLineParser = CommandLineParser() 16 | self.actionResolver = ActionResolver(app, self.commandLineParser) 17 | self.profile = Profile('test/vimrc') 18 | 19 | @patch('vimswitch.SwitchProfileAction.SwitchProfileAction') 20 | def test_doActions_resolvesSwitchProfileAction(self, mock): 21 | self.commandLineParser.action = 'switchProfile' 22 | self.commandLineParser.profile = self.profile 23 | 24 | self.actionResolver.doActions() 25 | 26 | switchProfileAction = mock.return_value 27 | self.assertEqual(switchProfileAction.profile, self.profile) 28 | self.assertTrue(switchProfileAction.execute.called) 29 | 30 | @patch('vimswitch.UpdateProfileAction.UpdateProfileAction') 31 | def test_doActions_resolvesUpdateProfileAction(self, mock): 32 | self.commandLineParser.action = 'updateProfile' 33 | self.commandLineParser.profile = self.profile 34 | 35 | self.actionResolver.doActions() 36 | 37 | updateProfileAction = mock.return_value 38 | self.assertEqual(updateProfileAction.profile, self.profile) 39 | self.assertTrue(updateProfileAction.execute.called) 40 | 41 | @patch('vimswitch.ShowCurrentProfileAction.ShowCurrentProfileAction') 42 | def test_doActions_resolvesShowCurrentProfileAction(self, mock): 43 | self.commandLineParser.action = 'showCurrentProfile' 44 | 45 | self.actionResolver.doActions() 46 | 47 | showCurrentProfileAction = mock.return_value 48 | self.assertTrue(showCurrentProfileAction.execute.called) 49 | 50 | @patch('vimswitch.ShowVersionAction.ShowVersionAction') 51 | def test_doActions_resolvesShowVersionAction(self, mock): 52 | self.commandLineParser.action = 'showVersion' 53 | 54 | self.actionResolver.doActions() 55 | 56 | showVersionAction = mock.return_value 57 | self.assertTrue(showVersionAction.execute.called) 58 | 59 | @patch('vimswitch.InvalidArgsAction.InvalidArgsAction') 60 | def test_doActions_resolvesInvalidArgsAction(self, mock): 61 | self.commandLineParser.action = 'invalidArgs' 62 | self.commandLineParser.errorMessage = 'testErrorMessage' 63 | self.commandLineParser.helpText = 'testHelpText' 64 | 65 | self.actionResolver.doActions() 66 | 67 | invalidArgsAction = mock.return_value 68 | self.assertTrue(invalidArgsAction.execute.called) 69 | self.assertEqual(invalidArgsAction.errorMessage, 'testErrorMessage') 70 | self.assertEqual(invalidArgsAction.helpText, 'testHelpText') 71 | 72 | @patch('vimswitch.InvalidArgsAction.InvalidArgsAction') 73 | def test_doActions_unknownAction_executesInvalidArgsAction(self, mock): 74 | self.commandLineParser.action = 'unknownAction' 75 | 76 | self.actionResolver.doActions() 77 | 78 | invalidArgsAction = mock.return_value 79 | self.assertTrue(invalidArgsAction.execute.called) 80 | 81 | @patch('vimswitch.UpdateProfileAction.UpdateProfileAction') 82 | def test_doActions_setsExitCode(self, mock): 83 | self.commandLineParser.action = 'updateProfile' 84 | self.commandLineParser.profile = self.profile 85 | action = mock.return_value 86 | action.exitCode = -1 87 | 88 | self.actionResolver.doActions() 89 | 90 | self.assertEqual(self.actionResolver.exitCode, -1) 91 | -------------------------------------------------------------------------------- /vimswitch/test/test_ApplicationDirs.py: -------------------------------------------------------------------------------- 1 | from .FileSystemTestCase import FileSystemTestCase 2 | from vimswitch.Settings import Settings 3 | from vimswitch.DiskIo import getDiskIo 4 | from vimswitch.ApplicationDirs import getApplicationDirs 5 | from vimswitch.Application import Application 6 | 7 | 8 | class TestApplicationDirs(FileSystemTestCase): 9 | def setUp(self): 10 | FileSystemTestCase.setUp(self) 11 | app = Application() 12 | app.settings = Settings(self.getWorkingDir()) 13 | self.settings = app.settings 14 | self.diskIo = getDiskIo(app) 15 | self.applicationDirs = getApplicationDirs(app) 16 | 17 | def test_createIfNone_noApplicationDirs_createsApplicationDirs(self): 18 | self.applicationDirs.createIfNone() 19 | 20 | dirExists = self.diskIo.dirExists 21 | self.assertTrue(dirExists(self.settings.configPath)) 22 | self.assertTrue(dirExists(self.settings.cachePath)) 23 | self.assertTrue(dirExists(self.settings.downloadsPath)) 24 | 25 | def test_createIfNone_applicationDirsExist_doesNothing(self): 26 | # Create dirs 27 | self.applicationDirs.createIfNone() 28 | 29 | # Application Dirs now exist. Create again 30 | self.applicationDirs.createIfNone() 31 | 32 | # No Exceptions should be raised 33 | -------------------------------------------------------------------------------- /vimswitch/test/test_CommandLineParser.py: -------------------------------------------------------------------------------- 1 | from .BaseTestCase import BaseTestCase 2 | from vimswitch.CommandLineParser import CommandLineParser 3 | from vimswitch.Profile import Profile 4 | 5 | 6 | class TestCommandLineParser(BaseTestCase): 7 | 8 | def setUp(self): 9 | self.commandLineParser = CommandLineParser() 10 | 11 | def test_parse_emptyArgs_setsShowCurrentProfileAction(self): 12 | argv = './vimswitch'.split() 13 | self.commandLineParser.parse(argv) 14 | self.assertEqual(self.commandLineParser.action, 'showCurrentProfile') 15 | 16 | def test_parse_setsSwitchProfileAction(self): 17 | argv = './vimswitch test/vimrc'.split() 18 | 19 | self.commandLineParser.parse(argv) 20 | 21 | self.assertEqual(self.commandLineParser.action, 'switchProfile') 22 | self.assertEqual(self.commandLineParser.profile, Profile('test/vimrc')) 23 | 24 | def test_parse_updateProfileAction(self): 25 | argv = './vimswitch --update test/vimrc'.split() 26 | 27 | self.commandLineParser.parse(argv) 28 | 29 | self.assertEqual(self.commandLineParser.action, 'updateProfile') 30 | self.assertEqual(self.commandLineParser.profile, Profile('test/vimrc')) 31 | 32 | def test_parse_updateWithoutProfile_setProfileToNone(self): 33 | argv = './vimswitch --update'.split() 34 | 35 | self.commandLineParser.parse(argv) 36 | 37 | self.assertEqual(self.commandLineParser.action, 'updateProfile') 38 | self.assertEqual(self.commandLineParser.profile, None) 39 | 40 | def test_parse_showVersionAction(self): 41 | argv = './vimswitch --version'.split() 42 | self.commandLineParser.parse(argv) 43 | self.assertEqual(self.commandLineParser.action, 'showVersion') 44 | 45 | def test_parse_tooManyArgs_setsErrorMessage(self): 46 | argv = './vimswitch test/vimrc foo'.split() 47 | 48 | self.commandLineParser.parse(argv) 49 | 50 | self.assertEqual(self.commandLineParser.action, 'invalidArgs') 51 | self.assertEqual(self.commandLineParser.errorMessage, 'unrecognized arguments: foo') 52 | 53 | def test_parse_invalidFlag_setsErrorMessage(self): 54 | argv = './vimswitch --foo test/vimrc'.split() 55 | 56 | self.commandLineParser.parse(argv) 57 | 58 | self.assertEqual(self.commandLineParser.action, 'invalidArgs') 59 | self.assertEqual(self.commandLineParser.errorMessage, 'unrecognized arguments: --foo') 60 | -------------------------------------------------------------------------------- /vimswitch/test/test_ConfigFile.py: -------------------------------------------------------------------------------- 1 | from .FileSystemTestCase import FileSystemTestCase 2 | from copy import deepcopy 3 | from vimswitch.Application import Application 4 | from vimswitch.Profile import Profile 5 | from vimswitch.Settings import Settings 6 | from vimswitch.ConfigFile import getConfigFile 7 | from vimswitch.DiskIo import getDiskIo 8 | import vimswitch.six.moves.configparser as configparser 9 | 10 | 11 | class TestConfigFile(FileSystemTestCase): 12 | def setUp(self): 13 | FileSystemTestCase.setUp(self) 14 | self.app = Application() 15 | self.configFile = getConfigFile(self.app) 16 | 17 | # ConfigFile.loadSettings() 18 | 19 | def test_loadSettings_allAttributes(self): 20 | self.copyDataToWorkingDir('vimswitchrc', 'vimswitchrc') 21 | configFilePath = self.getTestPath('vimswitchrc') 22 | settings = Settings() 23 | 24 | self.configFile.loadSettings(settings, configFilePath) 25 | 26 | self.assertEqual(settings.currentProfile, Profile('test/vimrc')) 27 | 28 | def test_loadSettings_fileDoesNotExist_settingsUnchanged(self): 29 | nonExistantPath = self.getTestPath('non_existant') 30 | settings = Settings() 31 | settingsCopy = deepcopy(settings) 32 | 33 | self.configFile.loadSettings(settings, nonExistantPath) 34 | 35 | self.assertEqual(settings, settingsCopy) 36 | 37 | def test_loadSettings_emptyFile_raisesError(self): 38 | diskIo = getDiskIo(self.app) 39 | emptyConfigFilePath = self.getTestPath('empty_vimswitchrc') 40 | diskIo.createFile(emptyConfigFilePath, '') 41 | settings = Settings() 42 | 43 | self.assertRaises(configparser.NoSectionError, self.configFile.loadSettings, settings, emptyConfigFilePath) 44 | 45 | def test_loadSettings_missingSection_raisesError(self): 46 | diskIo = getDiskIo(self.app) 47 | incorrectConfigFilePath = self.getTestPath('incorrect_vimswitchrc') 48 | diskIo.createFile(incorrectConfigFilePath, '[incorrect_section]') 49 | settings = Settings() 50 | 51 | self.assertRaises(configparser.NoSectionError, self.configFile.loadSettings, settings, incorrectConfigFilePath) 52 | 53 | # ConfigFile.saveSettings() 54 | 55 | def test_saveSettings_allAttributes(self): 56 | settings = Settings() 57 | settings.currentProfile = Profile('test/vimrc') 58 | configFilePath = self.getTestPath('vimswitchrc') 59 | 60 | self.configFile.saveSettings(settings, configFilePath) 61 | 62 | newSettings = Settings() 63 | self.configFile.loadSettings(newSettings, configFilePath) 64 | self.assertEqual(newSettings.currentProfile, Profile('test/vimrc')) 65 | 66 | def test_saveSettings_savesNoneValueAttributes(self): 67 | settings = Settings() 68 | settings.currentProfile = None 69 | configFilePath = self.getTestPath('vimswitchrc') 70 | 71 | self.configFile.saveSettings(settings, configFilePath) 72 | 73 | newSettings = Settings() 74 | self.configFile.loadSettings(newSettings, configFilePath) 75 | self.assertEqual(newSettings.currentProfile, None) 76 | 77 | def test_saveSettings_savesUnchangedSettings(self): 78 | settings = Settings() 79 | configFilePath = self.getTestPath('vimswitchrc') 80 | 81 | self.configFile.saveSettings(settings, configFilePath) 82 | 83 | newSettings = Settings() 84 | self.configFile.loadSettings(newSettings, configFilePath) 85 | self.assertEqual(newSettings, settings) 86 | -------------------------------------------------------------------------------- /vimswitch/test/test_FileDownloader.py: -------------------------------------------------------------------------------- 1 | from .FileSystemTestCase import FileSystemTestCase 2 | from .SimpleServer import SimpleServer 3 | from vimswitch.DiskIo import DiskIo 4 | from vimswitch.FileDownloader import FileDownloader 5 | from vimswitch.Settings import Settings 6 | from mock import MagicMock 7 | from nose.plugins.attrib import attr 8 | from email.message import Message 9 | 10 | 11 | @attr('slow') 12 | class TestFileDownloader(FileSystemTestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | cls.host = 'localhost' 16 | cls.server = SimpleServer(cls.getDataPath(''), cls.host) 17 | cls.server.start() 18 | cls.port = cls.server.getPort() 19 | 20 | @classmethod 21 | def tearDownClass(cls): 22 | cls.server.stop() 23 | 24 | def setUp(self): 25 | FileSystemTestCase.setUp(self) 26 | settings = Settings() 27 | settings.downloadsPath = self.getTestPath('') 28 | self.diskIo = DiskIo() 29 | self.fileDownloader = FileDownloader(settings, self.diskIo) 30 | 31 | # FileDownloader.download 32 | 33 | def test_download_downloadsFile(self): 34 | url = self.getLocalUrl('simple.txt') 35 | downloadPath = self.getTestPath('simple_downloaded.txt') 36 | 37 | self.fileDownloader.download(url, downloadPath) 38 | 39 | actual = self.diskIo.getFileContents(downloadPath) 40 | expected = 'test data' 41 | self.assertEqual(actual, expected) 42 | 43 | @attr('external') 44 | def test_download_downloadsFromHttps(self): 45 | url = 'https://github.com/priomsrb/vimrc/archive/master.zip' 46 | downloadDir = self.getTestPath('') 47 | 48 | downloadPath = self.fileDownloader.download(url, downloadDir) 49 | 50 | self.assertTrue(self.diskIo.fileExists(downloadPath)) 51 | 52 | def test_download_fileAlreadyExists_overwritesFile(self): 53 | url = self.getLocalUrl('simple.txt') 54 | downloadPath = self.getTestPath('simple_downloaded.txt') 55 | self.diskIo.createFile(downloadPath, 'previous data') 56 | 57 | self.fileDownloader.download(url, downloadPath) 58 | 59 | actual = self.diskIo.getFileContents(downloadPath) 60 | expected = 'test data' 61 | self.assertEqual(actual, expected) 62 | 63 | def test_download_nonExistantUrl_raisesError(self): 64 | nonExistantUrl = self.getLocalUrl('non_existant.txt') 65 | downloadDir = self.getTestPath('') 66 | 67 | self.assertRaises(IOError, self.fileDownloader.download, nonExistantUrl, downloadDir) 68 | 69 | def test_download_dirAsDestination_keepsOriginalFilename(self): 70 | url = self.getLocalUrl('simple.txt') 71 | downloadDir = self.getTestPath('') 72 | 73 | downloadPath = self.fileDownloader.download(url, downloadDir) 74 | 75 | self.assertEqual(downloadPath, self.getTestPath('simple.txt')) 76 | actual = self.diskIo.getFileContents(downloadPath) 77 | expected = 'test data' 78 | self.assertEqual(actual, expected) 79 | 80 | def test_download_withMissingFilename_generatesFilename(self): 81 | url = self.getLocalUrl('simple.txt') 82 | downloadDir = self.getTestPath('') 83 | self.fileDownloader._getDownloadFilename = MagicMock(FileDownloader._getDownloadFilename) 84 | self.fileDownloader._getDownloadFilename.return_value = '' 85 | 86 | downloadPath = self.fileDownloader.download(url, downloadDir) 87 | 88 | self.assertTrue(len(downloadPath) > 0) 89 | actual = self.diskIo.getFileContents(downloadPath) 90 | expected = 'test data' 91 | self.assertEqual(actual, expected) 92 | 93 | # FileDownloader._getDownloadFilename 94 | 95 | def test_getDownloadFilename_parsesUrl(self): 96 | url = 'http://example.com/foo/bar/url_filename.txt?q=1' 97 | headers = Message() 98 | 99 | filename = self.fileDownloader._getDownloadFilename(url, headers) 100 | 101 | self.assertEqual(filename, 'url_filename.txt') 102 | 103 | def test_getDownloadFilename_parsesHeaderFirst(self): 104 | url = 'http://example.com/foo/bar/url_filename.txt?q=1' 105 | headers = Message() 106 | headers['Server'] = 'SimpleHTTP/0.6 Python/2.7.5' 107 | headers['Date'] = 'Tue, 09 Sep 2014 02:51:53 GMT' 108 | headers['Content-type'] = 'text/plain' 109 | headers['Content-Length'] = '9' 110 | headers['Last-Modified'] = 'Mon, 08 Sep 2014 23:53:51 GMT' 111 | # content-disposition should specify the filename 112 | headers['content-disposition'] = 'attachment; filename=header_filename.txt' 113 | 114 | filename = self.fileDownloader._getDownloadFilename(url, headers) 115 | 116 | self.assertEqual(filename, 'header_filename.txt') 117 | 118 | def test_getDownloadFilename_invalidHeader_usesUrl(self): 119 | url = 'http://example.com/foo/bar/url_filename.txt?q=1' 120 | headers = Message() 121 | headers['Server'] = 'SimpleHTTP/0.6 Python/2.7.5' 122 | headers['Date'] = 'Tue, 09 Sep 2014 02:51:53 GMT' 123 | headers['Content-type'] = 'text/plain' 124 | headers['Content-Length'] = '9' 125 | headers['Last-Modified'] = 'Mon, 08 Sep 2014 23:53:51 GMT' 126 | # content-disposition should specify the filename 127 | headers['content-disposition'] = 'malformed_section' 128 | 129 | filename = self.fileDownloader._getDownloadFilename(url, headers) 130 | 131 | self.assertEqual(filename, 'url_filename.txt') 132 | 133 | # Helpers 134 | 135 | def getLocalUrl(self, path): 136 | return 'http://' + self.host + ':' + str(self.port) + '/' + path 137 | -------------------------------------------------------------------------------- /vimswitch/test/test_GithubZipballExtractor.py: -------------------------------------------------------------------------------- 1 | from .FileSystemTestCase import FileSystemTestCase 2 | from vimswitch.Application import Application 3 | from vimswitch.GithubZipballExtractor import getGithubZipballExtractor 4 | 5 | 6 | class TestGithubZipballExtractor(FileSystemTestCase): 7 | def setUp(self): 8 | FileSystemTestCase.setUp(self) 9 | app = Application() 10 | self.githubZipballExtractor = getGithubZipballExtractor(app) 11 | self.diskIo = app.diskIo 12 | 13 | def test_extract_extractsRepoFilesIntoPath(self): 14 | self.copyDataToWorkingDir('vimrc-master.zip', 'zipball.zip') 15 | zipballPath = self.getTestPath('zipball.zip') 16 | extractionDirPath = self.getTestPath('extractionDir') 17 | self.diskIo.createDir(extractionDirPath) 18 | 19 | self.githubZipballExtractor.extractZipball(zipballPath, extractionDirPath) 20 | 21 | self.assertFileContents('extractionDir/.vimrc', '" test vimrc data') 22 | self.assertDirExists('extractionDir/.vim') 23 | self.assertDirExists('extractionDir/.vim/plugin') 24 | self.assertFileContents('extractionDir/.vim/plugin/dummy_plugin.vim', '" dummy vim plugin') 25 | 26 | def test_extract_multipleRootDirs_raisesError(self): 27 | self.copyDataToWorkingDir('github_zipball_multiple_root_dirs.zip', 'zipball.zip') 28 | zipballPath = self.getTestPath('zipball.zip') 29 | extractionDirPath = self.getTestPath('extractionDir') 30 | self.diskIo.createDir(extractionDirPath) 31 | 32 | with self.assertRaises(IOError) as cm: 33 | self.githubZipballExtractor.extractZipball(zipballPath, extractionDirPath) 34 | 35 | self.assertRegexpMatches(str(cm.exception), 'Zipball .* has more than one root directory') 36 | 37 | def test_extract_noRootDir_raisesError(self): 38 | self.copyDataToWorkingDir('github_zipball_no_root_dir.zip', 'zipball.zip') 39 | zipballPath = self.getTestPath('zipball.zip') 40 | extractionDirPath = self.getTestPath('extractionDir') 41 | self.diskIo.createDir(extractionDirPath) 42 | 43 | with self.assertRaises(IOError) as cm: 44 | self.githubZipballExtractor.extractZipball(zipballPath, extractionDirPath) 45 | 46 | self.assertRegexpMatches(str(cm.exception), 'Zipball .* has no root directory') 47 | 48 | # Helpers 49 | 50 | def assertFileContents(self, path, expectedContents): 51 | diskIo = self.diskIo 52 | path = self.getTestPath(path) 53 | actualContents = diskIo.getFileContents(path) 54 | self.assertEqual(actualContents, expectedContents) 55 | 56 | def assertDirExists(self, path): 57 | diskIo = self.diskIo 58 | path = self.getTestPath(path) 59 | self.assertTrue(diskIo.dirExists(path)) 60 | -------------------------------------------------------------------------------- /vimswitch/test/test_Profile.py: -------------------------------------------------------------------------------- 1 | from .BaseTestCase import BaseTestCase 2 | from vimswitch.Profile import Profile 3 | 4 | 5 | class TestProfile(BaseTestCase): 6 | 7 | def test_getDirName_withSlashes_replacedByDot(self): 8 | profile = Profile('user/repo') 9 | self.assertEquals(profile.getDirName(), 'user.repo') 10 | profile = Profile('user/prefix.repo') 11 | self.assertEquals(profile.getDirName(), 'user.prefix.repo') 12 | 13 | def test_getDirName_withoutSlashes_isUnchanged(self): 14 | profile = Profile('repo') 15 | self.assertEquals(profile.getDirName(), 'repo') 16 | profile = Profile('prefix.repo') 17 | self.assertEquals(profile.getDirName(), 'prefix.repo') 18 | 19 | def test_equal(self): 20 | profile1 = Profile('user/repo') 21 | profile2 = Profile('user/repo') 22 | 23 | self.assertEqual(profile1, profile2) 24 | 25 | def test_notEqual(self): 26 | self.assertNotEqual(Profile('user/repo1'), Profile('user/repo2')) 27 | self.assertNotEqual(Profile('user/repo1'), None) 28 | self.assertNotEqual(None, Profile('user/repo2')) 29 | 30 | def test_repr(self): 31 | self.assertEqual(repr(Profile('user/repo')), "Profile('user/repo')") 32 | -------------------------------------------------------------------------------- /vimswitch/test/test_ProfileCache.py: -------------------------------------------------------------------------------- 1 | import os 2 | from vimswitch.Settings import Settings 3 | from vimswitch.DiskIo import DiskIo 4 | from vimswitch.ProfileCache import ProfileCache 5 | from vimswitch.Profile import Profile 6 | from .FileSystemTestCase import FileSystemTestCase 7 | 8 | 9 | class TestProfileCache(FileSystemTestCase): 10 | 11 | def setUp(self): 12 | FileSystemTestCase.setUp(self) 13 | self.diskIo = DiskIo() 14 | self.settings = Settings(self.getWorkingDir()) 15 | self.diskIo.createDirWithParents(self.settings.cachePath) 16 | self.profileCache = ProfileCache(self.settings, self.diskIo) 17 | self.testProfile = Profile('test/vimrc') 18 | 19 | # ProfileCache.contains 20 | 21 | def test_contains_whenDirExists_returnsTrue(self): 22 | profileDir = self.getTestPath('.vimswitch/profiles/test.vimrc') 23 | self.diskIo.createDir(profileDir) 24 | 25 | result = self.profileCache.contains(self.testProfile) 26 | 27 | self.assertTrue(result) 28 | 29 | def test_contains_whenDirDoesNotExist_returnsFalse(self): 30 | result = self.profileCache.contains(self.testProfile) 31 | self.assertFalse(result) 32 | 33 | # ProfileCache.delete 34 | 35 | def test_delete_deletesProfile(self): 36 | profileDir = self.getTestPath('.vimswitch/profiles/test.vimrc') 37 | vimrcFilePath = self.getTestPath('.vimswitch/profiles/test.vimrc/.vimrc') 38 | self.diskIo.createDir(profileDir) 39 | self.diskIo.createFile(vimrcFilePath, 'test data') 40 | 41 | self.profileCache.delete(self.testProfile) 42 | 43 | self.assertFalse(self.profileCache.contains(self.testProfile)) 44 | 45 | # ProfileCache.getProfileLocation 46 | 47 | def test_getProfileLocation(self): 48 | self.settings.cachePath = '/foo/bar/cachePath' 49 | profile = Profile('test/vimrc') 50 | result = self.profileCache.getProfileLocation(profile) 51 | self.assertEquals(result, os.path.normpath('/foo/bar/cachePath/test.vimrc')) 52 | 53 | # ProfileCache.createEmptyProfile 54 | 55 | def test_createEmptyProfile_profileDoesNotExist_createsProfileDir(self): 56 | profile = Profile('default') 57 | 58 | self.profileCache.createEmptyProfile(profile) 59 | 60 | profileDir = self.getTestPath('.vimswitch/profiles/default') 61 | self.assertTrue(self.diskIo.dirExists(profileDir)) 62 | -------------------------------------------------------------------------------- /vimswitch/test/test_ProfileCopier.py: -------------------------------------------------------------------------------- 1 | from . import Stubs 2 | from .BaseTestCase import BaseTestCase 3 | from mock import MagicMock 4 | from vimswitch.Profile import Profile 5 | from vimswitch.ProfileCache import ProfileCache 6 | from vimswitch.ProfileCopier import ProfileCopier 7 | from vimswitch.Settings import Settings 8 | import os 9 | 10 | 11 | class TestProfileCopier(BaseTestCase): 12 | def setUp(self): 13 | self.profile = Profile('test/vimrc') 14 | self.settings = Settings(os.path.normpath('/home/foo')) 15 | self.diskIo = Stubs.DiskIoStub() 16 | # We use the real ProfileCache (with stubbed dependencies) because 17 | # ProfileCache.getProfileLocation gets called by ProfileCopier 18 | self.profileCache = ProfileCache(self.settings, self.diskIo) 19 | self.profileDataIo = Stubs.ProfileDataIoStub() 20 | self.profileCopier = ProfileCopier(self.settings, self.profileCache, self.profileDataIo) 21 | 22 | # ProfileCopier.copyToHome(profile) 23 | 24 | def test_copyToHome_deletesHomeData(self): 25 | self.profileCopier.copyToHome(self.profile) 26 | 27 | homePath = os.path.normpath('/home/foo') 28 | self.profileDataIo.delete.assert_called_with(homePath) 29 | 30 | def test_copyToHome_deletesHomeDir_fromSettings(self): 31 | self.settings.homePath = 'testHomeDir' 32 | 33 | self.profileCopier.copyToHome(self.profile) 34 | 35 | self.profileDataIo.delete.assert_called_with(os.path.normpath('testHomeDir')) 36 | 37 | def test_copyToHome_copiesFromProfileToHome(self): 38 | self.profileCopier.copyToHome(self.profile) 39 | 40 | profilePath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 41 | homePath = os.path.normpath('/home/foo') 42 | self.profileDataIo.copy.assert_called_with(profilePath, homePath) 43 | 44 | def test_copyToHome_copiesFromCacheDir_fromSettings(self): 45 | self.settings.cachePath = 'testCachePath' 46 | 47 | self.profileCopier.copyToHome(self.profile) 48 | 49 | profilePath = os.path.normpath('testCachePath/test.vimrc') 50 | homePath = os.path.normpath('/home/foo') 51 | self.profileDataIo.copy.assert_called_with(profilePath, homePath) 52 | 53 | def test_copyToHome_copiesToHomeDir_fromSettings(self): 54 | self.settings.homePath = 'testHomeDir' 55 | 56 | self.profileCopier.copyToHome(self.profile) 57 | 58 | profilePath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 59 | homePath = os.path.normpath('testHomeDir') 60 | self.profileDataIo.copy.assert_called_with(profilePath, homePath) 61 | 62 | # ProfileCopier.copyFromHome 63 | 64 | def test_copyfromHome_profileInCache_doNotCreateEmptyProfile(self): 65 | self.profileCache.contains = MagicMock(spec=ProfileCache.contains, return_value=True) 66 | self.profileCache.createEmptyProfile = MagicMock(ProfileCache.createEmptyProfile) 67 | 68 | self.profileCopier.copyFromHome(self.profile) 69 | 70 | self.assertFalse(self.profileCache.createEmptyProfile.called) 71 | 72 | def test_copyfromHome_profileNotInCache_createEmptyProfile(self): 73 | self.profileCache.contains = MagicMock(spec=ProfileCache.contains, return_value=False) 74 | self.profileCache.createEmptyProfile = MagicMock(ProfileCache.createEmptyProfile) 75 | 76 | self.profileCopier.copyFromHome(self.profile) 77 | 78 | self.assertTrue(self.profileCache.createEmptyProfile.called) 79 | 80 | def test_copyfromHome_deletesProfileDir(self): 81 | self.profileCopier.copyFromHome(self.profile) 82 | 83 | profilePath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 84 | self.profileDataIo.delete.assert_called_with(profilePath) 85 | 86 | def test_copyfromHome_copiesFromHomeToProfile(self): 87 | self.profileCopier.copyFromHome(self.profile) 88 | 89 | homePath = os.path.normpath('/home/foo') 90 | profilePath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 91 | self.profileDataIo.copy.assert_called_with(homePath, profilePath) 92 | 93 | def test_copyfromHome_copiesFromHomeDir_fromSettings(self): 94 | self.settings.homePath = 'testHomeDir' 95 | self.profileCopier.copyFromHome(self.profile) 96 | 97 | homePath = os.path.normpath('testHomeDir') 98 | profilePath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 99 | self.profileDataIo.copy.assert_called_with(homePath, profilePath) 100 | 101 | def test_copyfromHome_copiesToCacheDir_fromSettings(self): 102 | self.settings.cachePath = 'testCacheDir' 103 | self.profileCopier.copyFromHome(self.profile) 104 | 105 | homePath = os.path.normpath('/home/foo') 106 | profilePath = os.path.normpath('testCacheDir/test.vimrc') 107 | self.profileDataIo.copy.assert_called_with(homePath, profilePath) 108 | -------------------------------------------------------------------------------- /vimswitch/test/test_ProfileDataIo.py: -------------------------------------------------------------------------------- 1 | from . import Stubs 2 | from .BaseTestCase import BaseTestCase 3 | from .TestHelpers import assertNoCall 4 | from vimswitch.ProfileDataIo import ProfileDataIo 5 | from vimswitch.Settings import Settings 6 | import os 7 | 8 | 9 | class TestProfileDataIo(BaseTestCase): 10 | def setUp(self): 11 | self.diskIo = Stubs.DiskIoStub() 12 | self.settings = Settings(os.path.normpath('/home/foo')) 13 | self.profileDataIo = ProfileDataIo(self.settings, self.diskIo) 14 | 15 | # ProfileDataIo.delete#files 16 | 17 | def test_delete_whenFileExists_deletesFile(self): 18 | profilePath = os.path.normpath('/home/foo') 19 | self.diskIo.fileExists.return_value = True 20 | 21 | self.profileDataIo.delete(profilePath) 22 | 23 | self.diskIo.deleteFile.assert_any_call(os.path.normpath('/home/foo/.vimrc')) 24 | 25 | def test_delete_whenFileDoesNotExists_doesNotDeleteFile(self): 26 | profilePath = os.path.normpath('/home/foo') 27 | self.diskIo.fileExists.return_value = False 28 | 29 | self.profileDataIo.delete(profilePath) 30 | 31 | assertNoCall(self.diskIo.deleteFile, os.path.normpath('/home/foo/.vimrc')) 32 | 33 | def test_delete_usesGetProfileFiles_fromSettings(self): 34 | profilePath = os.path.normpath('/home/foo') 35 | self.diskIo.fileExists.return_value = True 36 | self.settings.profileFiles = ['testProfileFile'] 37 | 38 | self.profileDataIo.delete(profilePath) 39 | 40 | self.diskIo.deleteFile.assert_any_call(os.path.normpath('/home/foo/testProfileFile')) 41 | 42 | def test_delete_deletesMultipleFiles(self): 43 | profilePath = os.path.normpath('/home/foo') 44 | self.diskIo.fileExists.return_value = True 45 | self.settings.profileFiles = ['file1', 'file2'] 46 | 47 | self.profileDataIo.delete(profilePath) 48 | 49 | self.diskIo.deleteFile.assert_any_call(os.path.normpath('/home/foo/file1')) 50 | self.diskIo.deleteFile.assert_any_call(os.path.normpath('/home/foo/file2')) 51 | 52 | # ProfileDataIo.delete#dirs 53 | 54 | def test_delete_whenDirExists_deletesDir(self): 55 | profilePath = os.path.normpath('/home/foo') 56 | self.diskIo.dirExists.return_value = True 57 | 58 | self.profileDataIo.delete(profilePath) 59 | 60 | self.diskIo.deleteDir.assert_any_call(os.path.normpath('/home/foo/.vim')) 61 | 62 | def test_delete_whenDirDoesNotExists_doesNotDeleteDir(self): 63 | profilePath = os.path.normpath('/home/foo') 64 | self.diskIo.dirExists.return_value = False 65 | 66 | self.profileDataIo.delete(profilePath) 67 | 68 | assertNoCall(self.diskIo.deleteDir, os.path.normpath('/home/foo/.vim')) 69 | 70 | def test_delete_usesGetProfileDirs_fromSettings(self): 71 | profilePath = os.path.normpath('/home/foo') 72 | self.diskIo.dirExists.return_value = True 73 | self.settings.profileDirs = ['testProfileDir'] 74 | 75 | self.profileDataIo.delete(profilePath) 76 | 77 | self.diskIo.deleteDir.assert_any_call(os.path.normpath('/home/foo/testProfileDir')) 78 | 79 | def test_delete_deletesMultipleDirs(self): 80 | profilePath = os.path.normpath('/home/foo') 81 | self.diskIo.dirExists.return_value = True 82 | self.settings.profileDirs = ['dir1', 'dir2'] 83 | 84 | self.profileDataIo.delete(profilePath) 85 | 86 | self.diskIo.deleteDir.assert_any_call(os.path.normpath('/home/foo/dir1')) 87 | self.diskIo.deleteDir.assert_any_call(os.path.normpath('/home/foo/dir2')) 88 | 89 | def test_delete_deletesFilesAndDirs(self): 90 | profilePath = os.path.normpath('/home/foo') 91 | self.diskIo.dirExists.return_value = True 92 | self.settings.profileFiles = ['file1', 'file2'] 93 | self.settings.profileDirs = ['dir1', 'dir2'] 94 | 95 | self.profileDataIo.delete(profilePath) 96 | 97 | self.diskIo.deleteFile.assert_any_call(os.path.normpath('/home/foo/file1')) 98 | self.diskIo.deleteFile.assert_any_call(os.path.normpath('/home/foo/file2')) 99 | self.diskIo.deleteDir.assert_any_call(os.path.normpath('/home/foo/dir1')) 100 | self.diskIo.deleteDir.assert_any_call(os.path.normpath('/home/foo/dir2')) 101 | 102 | # ProfileDataIo.copy#files 103 | 104 | def test_copy_whenFileExists_copiesFile(self): 105 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 106 | destPath = os.path.normpath('/home/foo') 107 | self.diskIo.fileExists.return_value = True 108 | 109 | self.profileDataIo.copy(srcPath, destPath) 110 | 111 | srcFile = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/.vimrc') 112 | destFile = os.path.normpath('/home/foo/.vimrc') 113 | self.diskIo.copyFile.assert_any_call(srcFile, destFile) 114 | 115 | def test_copy_whenFileDoesNotExists_doesNotCopyFile(self): 116 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 117 | destPath = os.path.normpath('/home/foo') 118 | self.diskIo.fileExists.return_value = False 119 | 120 | self.profileDataIo.copy(srcPath, destPath) 121 | 122 | srcFile = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/.vimrc') 123 | destFile = os.path.normpath('/home/foo/.vimrc') 124 | assertNoCall(self.diskIo.copyFile, srcFile, destFile) 125 | 126 | def test_copy_usesGetProfileFiles_fromSettings(self): 127 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 128 | destPath = os.path.normpath('/home/foo') 129 | self.diskIo.fileExists.return_value = True 130 | self.settings.profileFiles = ['testProfileFile'] 131 | 132 | self.profileDataIo.copy(srcPath, destPath) 133 | 134 | srcFile = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/testProfileFile') 135 | destFile = os.path.normpath('/home/foo/testProfileFile') 136 | self.diskIo.copyFile.assert_any_call(srcFile, destFile) 137 | 138 | def test_copy_copiesMultipleFiles(self): 139 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 140 | destPath = os.path.normpath('/home/foo') 141 | self.diskIo.fileExists.return_value = True 142 | self.settings.profileFiles = ['file1', 'file2'] 143 | 144 | self.profileDataIo.copy(srcPath, destPath) 145 | 146 | srcFile1 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/file1') 147 | destFile1 = os.path.normpath('/home/foo/file1') 148 | srcFile2 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/file2') 149 | destFile2 = os.path.normpath('/home/foo/file2') 150 | self.diskIo.copyFile.assert_any_call(srcFile1, destFile1) 151 | self.diskIo.copyFile.assert_any_call(srcFile2, destFile2) 152 | 153 | # ProfileDataIo.copy#dirs 154 | 155 | def test_copy_whenDirExists_copiesDir(self): 156 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 157 | destPath = os.path.normpath('/home/foo') 158 | self.diskIo.dirExists.return_value = True 159 | 160 | self.profileDataIo.copy(srcPath, destPath) 161 | 162 | srcDir = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/.vim') 163 | destDir = os.path.normpath('/home/foo/.vim') 164 | self.diskIo.copyDir.assert_any_call(srcDir, destDir) 165 | 166 | def test_copy_whenDirDoesNotExists_doesNotCopyDir(self): 167 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 168 | destPath = os.path.normpath('/home/foo') 169 | self.diskIo.dirExists.return_value = False 170 | 171 | self.profileDataIo.copy(srcPath, destPath) 172 | 173 | srcDir = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/.vim') 174 | destDir = os.path.normpath('/home/foo/.vim') 175 | assertNoCall(self.diskIo.copyDir, srcDir, destDir) 176 | 177 | def test_copy_usesGetProfileDirs_fromSettings(self): 178 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 179 | destPath = os.path.normpath('/home/foo') 180 | self.diskIo.dirExists.return_value = True 181 | self.settings.profileDirs = ['testProfileDir'] 182 | 183 | self.profileDataIo.copy(srcPath, destPath) 184 | 185 | srcDir = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/testProfileDir') 186 | destDir = os.path.normpath('/home/foo/testProfileDir') 187 | self.diskIo.copyDir.assert_any_call(srcDir, destDir) 188 | 189 | def test_copy_copiesMultipleDirs(self): 190 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 191 | destPath = os.path.normpath('/home/foo') 192 | self.diskIo.dirExists.return_value = True 193 | self.settings.profileDirs = ['dir1', 'dir2'] 194 | 195 | self.profileDataIo.copy(srcPath, destPath) 196 | 197 | srcDir1 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/dir1') 198 | destDir1 = os.path.normpath('/home/foo/dir1') 199 | srcDir2 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/dir2') 200 | destDir2 = os.path.normpath('/home/foo/dir2') 201 | self.diskIo.copyDir.assert_any_call(srcDir1, destDir1) 202 | self.diskIo.copyDir.assert_any_call(srcDir2, destDir2) 203 | 204 | def test_copy_copiesFilesAndDirs(self): 205 | srcPath = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc') 206 | destPath = os.path.normpath('/home/foo') 207 | self.diskIo.dirExists.return_value = True 208 | self.settings.profileFiles = ['file1', 'file2'] 209 | self.settings.profileDirs = ['dir1', 'dir2'] 210 | 211 | self.profileDataIo.copy(srcPath, destPath) 212 | 213 | srcFile1 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/file1') 214 | destFile1 = os.path.normpath('/home/foo/file1') 215 | srcFile2 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/file2') 216 | destFile2 = os.path.normpath('/home/foo/file2') 217 | srcDir1 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/dir1') 218 | destDir1 = os.path.normpath('/home/foo/dir1') 219 | srcDir2 = os.path.normpath('/home/foo/.vimswitch/profiles/test.vimrc/dir2') 220 | destDir2 = os.path.normpath('/home/foo/dir2') 221 | self.diskIo.copyFile.assert_any_call(srcFile1, destFile1) 222 | self.diskIo.copyFile.assert_any_call(srcFile2, destFile2) 223 | self.diskIo.copyDir.assert_any_call(srcDir1, destDir1) 224 | self.diskIo.copyDir.assert_any_call(srcDir2, destDir2) 225 | -------------------------------------------------------------------------------- /vimswitch/test/test_ProfileRetriever.py: -------------------------------------------------------------------------------- 1 | from .FakeFileDownloader import createFakeFileDownloader 2 | from .FileSystemTestCase import FileSystemTestCase 3 | from mock import patch 4 | from vimswitch.Application import Application 5 | from vimswitch.Profile import Profile 6 | from vimswitch.ProfileRetriever import getProfileRetriever 7 | from vimswitch.Settings import Settings 8 | from vimswitch.six import StringIO 9 | 10 | 11 | class TestProfileRetriever(FileSystemTestCase): 12 | def setUp(self): 13 | FileSystemTestCase.setUp(self) 14 | app = Application() 15 | 16 | app.settings = Settings(self.getWorkingDir()) 17 | app.fileDownloader = createFakeFileDownloader(app, self.getDataPath('fake_internet')) 18 | 19 | self.profileRetriever = getProfileRetriever(app) 20 | self.diskIo = app.diskIo 21 | self.diskIo.createDirWithParents(app.settings.downloadsPath) 22 | self.diskIo.createDirWithParents(app.settings.cachePath) 23 | 24 | def test_retrieve_retrievesProfile(self): 25 | profile = Profile('test/vimrc') 26 | 27 | self.profileRetriever.retrieve(profile) 28 | 29 | vimDirPath = self.getTestPath('.vimswitch/profiles/test.vimrc/.vim') 30 | vimrcFilePath = self.getTestPath('.vimswitch/profiles/test.vimrc/.vimrc') 31 | actualVimrcContent = self.diskIo.getFileContents(vimrcFilePath) 32 | expectedVimrcContent = '" test vimrc data' 33 | self.assertEqual(actualVimrcContent, expectedVimrcContent) 34 | self.assertTrue(self.diskIo.dirExists(vimDirPath)) 35 | 36 | def test_retrieve_profileAlreadyCached_overwritesProfile(self): 37 | profile = Profile('test/vimrc') 38 | profileDirPath = self.getTestPath('.vimswitch/profiles/test.vimrc') 39 | vimDirPath = self.getTestPath('.vimswitch/profiles/test.vimrc/.vim') 40 | vimrcFilePath = self.getTestPath('.vimswitch/profiles/test.vimrc/.vimrc') 41 | self.diskIo.createDir(profileDirPath) 42 | self.diskIo.createDir(vimDirPath) 43 | self.diskIo.createFile(vimrcFilePath, '" previous data') 44 | 45 | self.profileRetriever.retrieve(profile) 46 | 47 | actualVimrcContent = self.diskIo.getFileContents(vimrcFilePath) 48 | expectedVimrcContent = '" test vimrc data' 49 | self.assertEqual(actualVimrcContent, expectedVimrcContent) 50 | self.assertTrue(self.diskIo.dirExists(vimDirPath)) 51 | 52 | def test_retrieve_cannotDownloadProfile_raisesError(self): 53 | profile = Profile('non_existant/vimrc') 54 | 55 | self.assertRaises(IOError, self.profileRetriever.retrieve, profile) 56 | 57 | @patch('sys.stdout', new_callable=StringIO) 58 | def test_retrieve_prints(self, stdout): 59 | profile = Profile('test/vimrc') 60 | 61 | self.profileRetriever.retrieve(profile) 62 | 63 | self.assertStdout(stdout, 'Downloading profile from .*') 64 | -------------------------------------------------------------------------------- /vimswitch/test/test_ProfileUrlResolver.py: -------------------------------------------------------------------------------- 1 | from .BaseTestCase import BaseTestCase 2 | from vimswitch.Profile import Profile 3 | from vimswitch.ProfileUrlResolver import getProfileUrl 4 | 5 | 6 | class TestProfileUrlResolver(BaseTestCase): 7 | 8 | def test_getProfileUrl_convertsUserSlashRepo(self): 9 | profile = Profile('testuser/testrepo') 10 | 11 | url = getProfileUrl(profile) 12 | 13 | expectedUrl = 'https://github.com/testuser/testrepo/archive/master.zip' 14 | self.assertEqual(url, expectedUrl) 15 | -------------------------------------------------------------------------------- /vimswitch/test/test_ShowCurrentProfileAction.py: -------------------------------------------------------------------------------- 1 | from .BaseTestCase import BaseTestCase 2 | from mock import patch 3 | from vimswitch.Application import Application 4 | from vimswitch.Profile import Profile 5 | from vimswitch.ShowCurrentProfileAction import createShowCurrentProfileAction 6 | from vimswitch.six import StringIO 7 | 8 | 9 | class TestShowCurrentProfileAction(BaseTestCase): 10 | def setUp(self): 11 | self.app = Application() 12 | self.action = createShowCurrentProfileAction(self.app) 13 | 14 | @patch('sys.stdout', new_callable=StringIO) 15 | def test_execute_printsCurrentProfile(self, stdout): 16 | self.app.settings.currentProfile = Profile('test/vimrc') 17 | 18 | self.action.execute() 19 | 20 | self.assertStdout(stdout, 'Current profile: test/vimrc') 21 | 22 | @patch('sys.stdout', new_callable=StringIO) 23 | def test_execute_currentProfileIsNone_printsNone(self, stdout): 24 | self.app.settings.currentProfile = None 25 | 26 | self.action.execute() 27 | 28 | self.assertStdout(stdout, 'Current profile: None') 29 | -------------------------------------------------------------------------------- /vimswitch/test/test_SwitchProfileAction.py: -------------------------------------------------------------------------------- 1 | from .FakeFileDownloader import createFakeFileDownloader 2 | from .FileSystemTestCase import FileSystemTestCase 3 | from mock import MagicMock, patch 4 | from vimswitch.Application import Application 5 | from vimswitch.Profile import Profile 6 | from vimswitch.Settings import Settings 7 | from vimswitch.SwitchProfileAction import createSwitchProfileAction 8 | from vimswitch.six import StringIO 9 | import os 10 | 11 | 12 | class TestSwitchProfileAction(FileSystemTestCase): 13 | 14 | def setUp(self): 15 | FileSystemTestCase.setUp(self) 16 | self.app = Application() 17 | self.app.settings = Settings(self.getWorkingDir()) 18 | self.app.fileDownloader = createFakeFileDownloader(self.app, self.getDataPath('fake_internet')) 19 | self.switchProfileAction = createSwitchProfileAction(self.app) 20 | self.app.diskIo.createDirWithParents(self.app.settings.cachePath) 21 | self.app.diskIo.createDirWithParents(self.app.settings.downloadsPath) 22 | self.profile = Profile('test/vimrc') 23 | 24 | def test_switchToProfile_defaultProfileDoesNotExist_createsDefaultProfile(self): 25 | self.switchProfileAction.profile = self.profile 26 | 27 | self.switchProfileAction.execute() 28 | 29 | defaultProfile = self.app.settings.defaultProfile 30 | self.assertTrue(self.app.profileCache.contains(defaultProfile)) 31 | 32 | def test_switchToProfile_profileNotInCache_downloadsProfile(self): 33 | self.switchProfileAction.profile = self.profile 34 | self.switchProfileAction.execute() 35 | self.assertTrue(self.app.profileCache.contains(self.profile)) 36 | 37 | def test_switchToProfile_profileInCache_doesNotDownloadProfile(self): 38 | self.app.fileDownloader.download = MagicMock(side_effect=AssertionError('Profile should not be downloaded')) 39 | self.app.profileCache.createEmptyProfile(self.profile) 40 | self.switchProfileAction.profile = self.profile 41 | 42 | self.switchProfileAction.execute() 43 | 44 | def test_switchToProfile_copiesProfileToHome(self): 45 | self.switchProfileAction.profile = self.profile 46 | 47 | self.switchProfileAction.execute() 48 | 49 | expectedVimrc = '" test vimrc data' 50 | actualVimrc = self.app.diskIo.getFileContents(self.getTestPath('.vimrc')) 51 | self.assertEqual(expectedVimrc, actualVimrc) 52 | vimDirPath = self.getTestPath('.vim') 53 | self.assertTrue(self.app.diskIo.dirExists(vimDirPath)) 54 | 55 | def test_switchToProfile_copiesHomeToCache(self): 56 | vimrcPath = self.getTestPath('.vimrc') 57 | vimDirPath = self.getTestPath('.vim') 58 | self.app.diskIo.createFile(vimrcPath, '" default vimrc') 59 | self.app.diskIo.createDir(vimDirPath) 60 | self.switchProfileAction.profile = self.profile 61 | 62 | self.switchProfileAction.execute() 63 | 64 | defaultProfile = self.app.settings.defaultProfile 65 | cachedVimrcPath = os.path.join(self.app.profileCache.getProfileLocation(defaultProfile), '.vimrc') 66 | expectedVimrc = '" default vimrc' 67 | actualVimrc = self.app.diskIo.getFileContents(cachedVimrcPath) 68 | self.assertEqual(expectedVimrc, actualVimrc) 69 | cachedVimDirPath = os.path.join(self.app.profileCache.getProfileLocation(defaultProfile), '.vim') 70 | self.assertTrue(self.app.diskIo.dirExists(cachedVimDirPath)) 71 | 72 | def test_switchToProfile_savesProfileChangesToCache(self): 73 | self.switchProfileAction.profile = self.profile 74 | self.switchProfileAction.execute() 75 | # Now we make changes to the profile 76 | vimrcPath = self.getTestPath('.vimrc') 77 | vimDirPath = self.getTestPath('.vim') 78 | self.app.diskIo.createFile(vimrcPath, '" updated vimrc') # Edit file 79 | self.app.diskIo.deleteDir(vimDirPath) # Delete dir 80 | defaultProfile = self.app.settings.defaultProfile 81 | self.switchProfileAction.profile = defaultProfile 82 | 83 | self.switchProfileAction.execute() 84 | 85 | # Assert .vimrc updated 86 | cachedVimrcPath = os.path.join(self.app.profileCache.getProfileLocation(self.profile), '.vimrc') 87 | expectedVimrc = '" updated vimrc' 88 | actualVimrc = self.app.diskIo.getFileContents(cachedVimrcPath) 89 | self.assertEqual(expectedVimrc, actualVimrc) 90 | # Assert .vim deleted 91 | cachedVimDirPath = os.path.join(self.app.profileCache.getProfileLocation(defaultProfile), '.vim') 92 | self.assertFalse(self.app.diskIo.dirExists(cachedVimDirPath)) 93 | 94 | def test_switchToProfile_updateFlagSet_updatesCachedProfile(self): 95 | # Do an initial download of the profile 96 | self.switchProfileAction.profile = self.profile 97 | self.switchProfileAction.execute() 98 | # Update the profile on the internet by using the version at fake_internet2 99 | self.app.fileDownloader.root = self.getDataPath('fake_internet2') 100 | self.switchProfileAction.update = True 101 | self.switchProfileAction.profile = self.profile 102 | 103 | self.switchProfileAction.execute() 104 | 105 | self.assertFileContents('.vimrc', '" updated vimrc data') 106 | self.assertFileContents('.vimswitch/profiles/test.vimrc/.vimrc', '" updated vimrc data') 107 | vimDirPath = self.getTestPath('.vim') 108 | self.assertTrue(self.app.diskIo.dirExists(vimDirPath)) 109 | 110 | def test_switchToProfile_setsCurrentProfile(self): 111 | self.assertNotEqual(self.app.settings.currentProfile, self.profile) 112 | self.switchProfileAction.profile = self.profile 113 | 114 | self.switchProfileAction.execute() 115 | 116 | self.assertEqual(self.app.settings.currentProfile, self.profile) 117 | 118 | @patch('sys.stdout', new_callable=StringIO) 119 | def test_switchToProfile_prints(self, stdout): 120 | self.switchProfileAction.profile = self.profile 121 | 122 | self.switchProfileAction.execute() 123 | 124 | self.assertStdout(stdout, """ 125 | Saving profile: default 126 | Downloading profile from https://github.com/test/vimrc/archive/master.zip 127 | Switched to profile: test/vimrc 128 | """) 129 | 130 | # Helpers 131 | 132 | def assertFileContents(self, path, expectedContents): 133 | diskIo = self.app.diskIo 134 | path = self.getTestPath(path) 135 | actualContents = diskIo.getFileContents(path) 136 | self.assertEqual(actualContents, expectedContents) 137 | -------------------------------------------------------------------------------- /vimswitch/test/test_UpdateProfileAction.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock, patch 2 | from vimswitch.Application import Application 3 | from vimswitch.Profile import Profile 4 | from vimswitch.Settings import getSettings 5 | from vimswitch.SwitchProfileAction import SwitchProfileAction 6 | from vimswitch.UpdateProfileAction import UpdateProfileAction 7 | from vimswitch.six import StringIO 8 | from .BaseTestCase import BaseTestCase 9 | 10 | 11 | class TestUpdateProfileAction(BaseTestCase): 12 | def setUp(self): 13 | app = Application() 14 | self.settings = getSettings(app) 15 | self.switchProfileAction = MagicMock(SwitchProfileAction) 16 | self.updateProfileAction = UpdateProfileAction(self.settings, self.switchProfileAction) 17 | 18 | def test_execute_withProfile_updatesProfile(self): 19 | self.updateProfileAction.profile = Profile('test/vimrc') 20 | 21 | self.updateProfileAction.execute() 22 | 23 | self.assertTrue(self.switchProfileAction.execute.called) 24 | self.assertEqual(self.switchProfileAction.profile, Profile('test/vimrc')) 25 | self.assertEqual(self.switchProfileAction.update, True) 26 | 27 | def test_execute_withoutProfile_updatesCurrentProfile(self): 28 | self.settings.currentProfile = Profile('test/currentProfile') 29 | self.updateProfileAction.profile = None 30 | 31 | self.updateProfileAction.execute() 32 | 33 | self.assertTrue(self.switchProfileAction.execute.called) 34 | self.assertEqual(self.switchProfileAction.profile, Profile('test/currentProfile')) 35 | self.assertEqual(self.switchProfileAction.update, True) 36 | 37 | @patch('sys.stdout', new_callable=StringIO) 38 | def test_execute_withDefaultProfile_printError(self, stdout): 39 | self.updateProfileAction.profile = Profile('default') 40 | 41 | self.updateProfileAction.execute() 42 | 43 | self.assertFalse(self.switchProfileAction.execute.called) 44 | self.assertEqual(self.updateProfileAction.exitCode, -1) 45 | self.assertStdout(stdout, 'Cannot update default profile') 46 | -------------------------------------------------------------------------------- /vimswitch/test/test_VimSwitch.py: -------------------------------------------------------------------------------- 1 | from .FakeFileDownloader import createFakeFileDownloader 2 | from .FileSystemTestCase import FileSystemTestCase 3 | from vimswitch.Settings import Settings 4 | from mock import patch 5 | from vimswitch.Application import Application 6 | from vimswitch.VimSwitch import VimSwitch 7 | from vimswitch.six import StringIO 8 | import platform 9 | import vimswitch.version 10 | 11 | 12 | class TestVimSwitch(FileSystemTestCase): 13 | 14 | def setUp(self): 15 | FileSystemTestCase.setUp(self) 16 | self.fakeInternetRoot = self.getDataPath('fake_internet') 17 | self.resetApplication() 18 | 19 | # Switch Profile 20 | 21 | def test_switchProfile_createsApplicationDirs(self): 22 | exitCode = self.runMain('./vimswitch test/vimrc') 23 | 24 | self.assertEqual(exitCode, 0) 25 | # Assert application dirs exist 26 | settings = self.app.settings 27 | self.assertDirExists(settings.configPath) 28 | self.assertDirExists(settings.cachePath) 29 | self.assertDirExists(settings.downloadsPath) 30 | 31 | @patch('sys.stdout', new_callable=StringIO) 32 | def test_switchProfile_switchToRemoteProfile(self, stdout): 33 | self.copyDataToWorkingDir('home/.vimrc', '.vimrc') 34 | self.copyDataToWorkingDir('home/.vim', '.vim') 35 | 36 | exitCode = self.runMain('./vimswitch test/vimrc') 37 | 38 | self.assertEqual(exitCode, 0, stdout.getvalue()) 39 | # Assert default profile is created 40 | self.assertFileContents('.vimswitch/profiles/default/.vimrc', '" home vimrc data') 41 | self.assertDirExists('.vimswitch/profiles/default/.vim') 42 | # Assert home profile is replaced by downloaded profile 43 | self.assertFileContents('.vimrc', '" test vimrc data') 44 | self.assertDirExists('.vim') 45 | # Assert stdout 46 | self.assertStdout(stdout, """ 47 | Saving profile: default 48 | Downloading profile from https://github.com/test/vimrc/archive/master.zip 49 | Switched to profile: test/vimrc 50 | """) 51 | 52 | @patch('sys.stdout', new_callable=StringIO) 53 | def test_switchProfile_switchToNonExistantProfile_showsError(self, stdout): 54 | self.copyDataToWorkingDir('home/.vimrc', '.vimrc') 55 | self.copyDataToWorkingDir('home/.vim', '.vim') 56 | 57 | exitCode = self.runMain('./vimswitch non_existant_profile') 58 | 59 | self.assertEqual(exitCode, -1, stdout.getvalue()) 60 | # Assert home profile is unchanged 61 | self.assertFileContents('.vimrc', '" home vimrc data') 62 | self.assertDirExists('.vim') 63 | # Assert stdout 64 | self.assertStdout(stdout, """ 65 | Saving profile: default 66 | Downloading profile from https://github.com/non_existant_profile/archive/master.zip 67 | Error: .* 404 File not found 68 | """) 69 | 70 | @patch('sys.stdout', new_callable=StringIO) 71 | def test_switchProfile_switchToAnotherProfile(self, stdout): 72 | # Switch to an initial profile 73 | self.runMain('./vimswitch test/vimrc') 74 | self.resetStdout(stdout) 75 | 76 | # Now switch to another profile 77 | exitCode = self.runMain('./vimswitch test2/vimrc') 78 | 79 | self.assertEqual(exitCode, 0, stdout.getvalue()) 80 | # Assert current profile is now test2/vimrc 81 | self.assertFileContents('.vimrc', '" test2 vimrc data') 82 | self.assertDirExists('.vim') 83 | # Assert stdout 84 | self.assertStdout(stdout, """ 85 | Saving profile: test/vimrc 86 | Downloading profile from https://github.com/test2/vimrc/archive/master.zip 87 | Switched to profile: test2/vimrc 88 | """) 89 | 90 | @patch('sys.stdout', new_callable=StringIO) 91 | def test_switchProfile_switchToCachedProfile(self, stdout): 92 | # Download 2 profiles 93 | self.runMain('./vimswitch test/vimrc') 94 | self.runMain('./vimswitch test2/vimrc') 95 | self.resetStdout(stdout) 96 | 97 | # Switch back to the first profile 98 | exitCode = self.runMain('./vimswitch test/vimrc') 99 | 100 | self.assertEqual(exitCode, 0, stdout.getvalue()) 101 | # Assert current profile is now test/vimrc 102 | self.assertFileContents('.vimrc', '" test vimrc data') 103 | self.assertDirExists('.vim') 104 | # Assert stdout 105 | self.assertStdout(stdout, """ 106 | Saving profile: test2/vimrc 107 | Switched to profile: test/vimrc 108 | """) 109 | 110 | def test_switchProfile_switchFromEmptyDefaultProfile(self): 111 | exitCode = self.runMain('./vimswitch test/vimrc') 112 | 113 | self.assertEqual(exitCode, 0) 114 | # Assert default profile is created and empty 115 | self.assertDirExists('.vimswitch/profiles/default') 116 | self.assertDirEmpty('.vimswitch/profiles/default') 117 | # Assert home profile is replaced by downloaded profile 118 | self.assertFileContents('.vimrc', '" test vimrc data') 119 | self.assertDirExists('.vim') 120 | 121 | def test_switchProfile_switchToEmptyDefaultProfile(self): 122 | # Switch to non-default profile 123 | self.runMain('./vimswitch test/vimrc') 124 | 125 | # Now switch back to default profile 126 | exitCode = self.runMain('./vimswitch default') 127 | 128 | self.assertEqual(exitCode, 0) 129 | # Assert home profile is now empty 130 | self.assertPathDoesNotExist('.vimrc') 131 | self.assertPathDoesNotExist('.vim') 132 | 133 | @patch('sys.stdout', new_callable=StringIO) 134 | def test_switchProfile_switchFromReadOnlyProfileData(self, stdout): 135 | self.copyDataToWorkingDir('home/.vimrc', '.vimrc') 136 | self.copyDataToWorkingDir('home/.vim', '.vim') 137 | self.setReadOnly('.vim/plugin/dummy_plugin.vim', True) 138 | 139 | exitCode = self.runMain('./vimswitch test/vimrc') 140 | 141 | self.assertEqual(exitCode, 0, stdout.getvalue()) 142 | # Assert default profile is created 143 | self.assertFileContents('.vimswitch/profiles/default/.vimrc', '" home vimrc data') 144 | self.assertFileContents('.vimswitch/profiles/default/.vim/plugin/dummy_plugin.vim', '" dummy home vim plugin') 145 | self.assertDirExists('.vimswitch/profiles/default/.vim') 146 | # Assert home profile is replaced by downloaded profile 147 | self.assertFileContents('.vimrc', '" test vimrc data') 148 | self.assertDirExists('.vim') 149 | # Assert home profile no longer contains read-only file 150 | self.assertPathDoesNotExist('.vim/plugin/dummy_plugin.vim') 151 | # Assert stdout 152 | self.assertStdout(stdout, """ 153 | Saving profile: default 154 | Downloading profile from https://github.com/test/vimrc/archive/master.zip 155 | Switched to profile: test/vimrc 156 | """) 157 | 158 | @patch('sys.stdout', new_callable=StringIO) 159 | def test_switchProfile_savesChangesToCurrentProfile(self, stdout): 160 | self.copyDataToWorkingDir('home/.vimrc', '.vimrc') 161 | self.copyDataToWorkingDir('home/.vim', '.vim') 162 | self.runMain('./vimswitch test/vimrc') 163 | self.resetStdout(stdout) 164 | # Make changes to the test/vimrc profile 165 | self.createFile('.vimrc', '" updated vimrc data') 166 | self.deleteDir('.vim') 167 | 168 | exitCode = self.runMain('./vimswitch default') 169 | 170 | self.assertEqual(exitCode, 0, stdout.getvalue()) 171 | # Assert .vimrc changes saved 172 | self.assertFileContents('.vimswitch/profiles/test.vimrc/.vimrc', '" updated vimrc data') 173 | # Assert .vim dir deleted 174 | self.assertPathDoesNotExist('.vimswitch/profiles/test.vimrc/.vim') 175 | # Assert stdout 176 | self.assertStdout(stdout, """ 177 | Saving profile: test/vimrc 178 | Switched to profile: default 179 | """) 180 | 181 | def test_switchProfile_ignoresNonProfileFiles(self): 182 | # We will check that the following files and dirs still exist after 183 | # switching profiles 184 | filePaths = [ 185 | 'test' 186 | 'test.txt' 187 | '.test' 188 | '.vimperatorrc' 189 | '.viminfo' 190 | '_viminfo' 191 | ] 192 | dirPaths = [ 193 | 'testDir' 194 | 'testDir.txt' 195 | '.testDir' 196 | '.vimperator' 197 | ] 198 | for filePath in filePaths: 199 | self.createFile(filePath, 'test content') 200 | for dirPath in dirPaths: 201 | self.createDir(dirPath) 202 | self.copyDataToWorkingDir('home/.vimrc', '_vimrc') 203 | self.copyDataToWorkingDir('home/.vim', '_vim') 204 | 205 | exitCode = self.runMain('./vimswitch test/vimrc') 206 | 207 | self.assertEqual(exitCode, 0) 208 | # Assert all the non-profile files and dirs still exist 209 | for filePath in filePaths: 210 | self.assertFileContents(filePath, 'test content') 211 | for dirPath in dirPaths: 212 | self.assertDirExists(dirPath) 213 | 214 | def test_switchProfile_switchFromWindowsProfile_movesWindowsProfileDataToCache(self): 215 | self.copyDataToWorkingDir('home/.vimrc', '_vimrc') 216 | self.copyDataToWorkingDir('home/.vim', '_vim') 217 | 218 | exitCode = self.runMain('./vimswitch test/vimrc') 219 | 220 | self.assertEqual(exitCode, 0) 221 | # Assert windows profile files are deleted 222 | self.assertPathDoesNotExist('_vimrc') 223 | self.assertPathDoesNotExist('_vim') 224 | # Assert windows profile moved to cache 225 | self.assertFileContents('.vimswitch/profiles/default/_vimrc', '" home vimrc data') 226 | self.assertDirExists('.vimswitch/profiles/default/_vim') 227 | # Assert home profile is replaced by downloaded profile 228 | self.assertFileContents('.vimrc', '" test vimrc data') 229 | self.assertDirExists('.vim') 230 | 231 | # Update profile 232 | 233 | @patch('sys.stdout', new_callable=StringIO) 234 | def test_updateProfile_redownloadsCachedProfile(self, stdout): 235 | self.runMain('./vimswitch test/vimrc') 236 | self.resetStdout(stdout) 237 | # Update profile on internet 238 | self.fakeInternetRoot = self.getDataPath('fake_internet2') 239 | 240 | # Now we update test/vimrc 241 | exitCode = self.runMain('./vimswitch --update test/vimrc') 242 | 243 | self.assertEqual(exitCode, 0, stdout.getvalue()) 244 | self.assertFileContents('.vimrc', '" updated vimrc data') 245 | self.assertFileContents('.vimswitch/profiles/test.vimrc/.vimrc', '" updated vimrc data') 246 | self.assertDirExists('.vimswitch/profiles/test.vimrc/.vim') 247 | self.assertStdout(stdout, """ 248 | Saving profile: test/vimrc 249 | Downloading profile from https://github.com/test/vimrc/archive/master.zip 250 | Switched to profile: test/vimrc 251 | """) 252 | 253 | @patch('sys.stdout', new_callable=StringIO) 254 | def test_updateProfile_switchesToProfile(self, stdout): 255 | self.runMain('./vimswitch test/vimrc') 256 | self.runMain('./vimswitch test2/vimrc') 257 | self.resetStdout(stdout) 258 | # Update profile on internet 259 | self.fakeInternetRoot = self.getDataPath('fake_internet2') 260 | 261 | # Now we update test/vimrc 262 | exitCode = self.runMain('./vimswitch --update test/vimrc') 263 | 264 | self.assertEqual(exitCode, 0, stdout.getvalue()) 265 | self.assertFileContents('.vimrc', '" updated vimrc data') 266 | self.assertFileContents('.vimswitch/profiles/test.vimrc/.vimrc', '" updated vimrc data') 267 | self.assertDirExists('.vimswitch/profiles/test.vimrc/.vim') 268 | self.assertStdout(stdout, """ 269 | Saving profile: test2/vimrc 270 | Downloading profile from https://github.com/test/vimrc/archive/master.zip 271 | Switched to profile: test/vimrc 272 | """) 273 | 274 | @patch('sys.stdout', new_callable=StringIO) 275 | def test_updateProfile_noArguments_updatesCurrentProfile(self, stdout): 276 | self.runMain('./vimswitch test/vimrc') 277 | self.resetStdout(stdout) 278 | # Update profile on internet 279 | self.fakeInternetRoot = self.getDataPath('fake_internet2') 280 | 281 | # Now we update test/vimrc 282 | exitCode = self.runMain('./vimswitch --update') 283 | 284 | self.assertEqual(exitCode, 0, stdout.getvalue()) 285 | self.assertFileContents('.vimrc', '" updated vimrc data') 286 | self.assertFileContents('.vimswitch/profiles/test.vimrc/.vimrc', '" updated vimrc data') 287 | self.assertDirExists('.vimswitch/profiles/test.vimrc/.vim') 288 | self.assertStdout(stdout, """ 289 | Saving profile: test/vimrc 290 | Downloading profile from https://github.com/test/vimrc/archive/master.zip 291 | Switched to profile: test/vimrc 292 | """) 293 | 294 | @patch('sys.stdout', new_callable=StringIO) 295 | def test_updateProfile_downloadsUncachedProfile(self, stdout): 296 | exitCode = self.runMain('./vimswitch --update test/vimrc') 297 | 298 | self.assertEqual(exitCode, 0, stdout.getvalue()) 299 | self.assertFileContents('.vimrc', '" test vimrc data') 300 | self.assertFileContents('.vimswitch/profiles/test.vimrc/.vimrc', '" test vimrc data') 301 | self.assertDirExists('.vimswitch/profiles/test.vimrc/.vim') 302 | self.assertStdout(stdout, """ 303 | Saving profile: default 304 | Downloading profile from https://github.com/test/vimrc/archive/master.zip 305 | Switched to profile: test/vimrc 306 | """) 307 | 308 | @patch('sys.stdout', new_callable=StringIO) 309 | def test_updateProfile_withDefaultProfile_showsError(self, stdout): 310 | self.runMain('./vimswitch test/vimrc') 311 | self.resetStdout(stdout) 312 | 313 | exitCode = self.runMain('./vimswitch --update default') 314 | 315 | self.assertEqual(exitCode, -1) 316 | self.assertFileContents('.vimrc', '" test vimrc data') 317 | self.assertFileContents('.vimswitch/profiles/test.vimrc/.vimrc', '" test vimrc data') 318 | self.assertDirExists('.vimswitch/profiles/test.vimrc/.vim') 319 | self.assertStdout(stdout, """ 320 | Cannot update default profile 321 | """) 322 | 323 | @patch('sys.stdout', new_callable=StringIO) 324 | def test_updateProfile_withDefaultProfileAndNoArguments_showsError(self, stdout): 325 | exitCode = self.runMain('./vimswitch --update') 326 | 327 | self.assertEqual(exitCode, -1) 328 | self.assertStdout(stdout, """ 329 | Cannot update default profile 330 | """) 331 | 332 | # Show current profile 333 | 334 | @patch('sys.stdout', new_callable=StringIO) 335 | def test_noArguments_showsCurrentProfile(self, stdout): 336 | self.runMain('./vimswitch test/vimrc') # Sets current profile to test/vimrc 337 | self.resetStdout(stdout) 338 | 339 | exitCode = self.runMain('./vimswitch') 340 | 341 | self.assertEqual(exitCode, 0, stdout.getvalue()) 342 | # Assert stdout 343 | self.assertStdout(stdout, """ 344 | Current profile: test/vimrc 345 | """) 346 | 347 | @patch('sys.stdout', new_callable=StringIO) 348 | def test_noArgumentsAndNoCurrentProfile_showsCurrentProfileIsNone(self, stdout): 349 | exitCode = self.runMain('./vimswitch') 350 | 351 | self.assertEqual(exitCode, 0, stdout.getvalue()) 352 | # Assert stdout 353 | self.assertStdout(stdout, """ 354 | Current profile: None 355 | """) 356 | 357 | @patch('sys.stdout', new_callable=StringIO) 358 | def test_help(self, stdout): 359 | argsList = [ 360 | './vimswitch -h', 361 | './vimswitch --help' 362 | ] 363 | 364 | for args in argsList: 365 | exitCode = self.runMain(args) 366 | 367 | self.assertEqual(exitCode, -1, stdout.getvalue()) 368 | # Assert stdout 369 | helpRegex = """ 370 | usage: vimswitch [-h] [-u] [-v] [profile] 371 | 372 | A utility for switching between vim profiles. 373 | 374 | positional arguments: 375 | profile 376 | 377 | optional arguments: 378 | -h, --help show this help message and exit 379 | -u, --update download profile again 380 | -v, --version show version 381 | """ 382 | helpRegex = helpRegex.replace('[', r'\[') 383 | helpRegex = helpRegex.replace(']', r'\]') 384 | self.assertStdout(stdout, helpRegex) 385 | self.resetStdout(stdout) 386 | 387 | @patch('sys.stdout', new_callable=StringIO) 388 | def test_tooManyArgs_showsErrorMessage(self, stdout): 389 | exitCode = self.runMain('./vimswitch test/vimrc extra_argument') 390 | 391 | self.assertEqual(exitCode, -1, stdout.getvalue()) 392 | # Assert stdout 393 | self.assertStdout(stdout, """ 394 | unrecognized arguments: extra_argument 395 | usage: vimswitch .* 396 | .* 397 | """) 398 | 399 | @patch('sys.stdout', new_callable=StringIO) 400 | def test_version(self, stdout): 401 | argsList = [ 402 | './vimswitch -v', 403 | './vimswitch --version' 404 | ] 405 | 406 | for args in argsList: 407 | exitCode = self.runMain(args) 408 | 409 | self.assertEqual(exitCode, 0, stdout.getvalue()) 410 | # Assert stdout 411 | appVersion = vimswitch.version.__version__ 412 | pythonVersion = platform.python_version() 413 | versionRegex = 'vimswitch %s (python %s)' % (appVersion, pythonVersion) 414 | versionRegex = versionRegex.replace('(', r'\(') 415 | versionRegex = versionRegex.replace(')', r'\)') 416 | self.assertStdout(stdout, versionRegex) 417 | self.resetStdout(stdout) 418 | # appVersion must have at least 3 chars 419 | self.assertTrue(len(appVersion) >= len('0.0')) 420 | 421 | # Helpers 422 | 423 | def runMain(self, args): 424 | self.resetApplication() 425 | argv = args.split() 426 | exitCode = self.vimSwitch.main(argv) 427 | return exitCode 428 | 429 | def resetApplication(self): 430 | """ 431 | Resets the state of the application. This needs to be called every time 432 | before running vimswitch.main() 433 | """ 434 | self.app = Application() 435 | self.app.settings = Settings(self.getWorkingDir()) 436 | self.app.fileDownloader = createFakeFileDownloader(self.app, self.fakeInternetRoot) 437 | self.vimSwitch = VimSwitch(self.app) 438 | self.vimSwitch.raiseExceptions = True 439 | 440 | def createFile(self, path, contents): 441 | diskIo = self.app.diskIo 442 | path = self.getTestPath(path) 443 | diskIo.createFile(path, contents) 444 | 445 | def createDir(self, path): 446 | diskIo = self.app.diskIo 447 | path = self.getTestPath(path) 448 | diskIo.createDir(path) 449 | 450 | def deleteDir(self, path): 451 | diskIo = self.app.diskIo 452 | path = self.getTestPath(path) 453 | diskIo.deleteDir(path) 454 | 455 | def setReadOnly(self, path, readOnly): 456 | diskIo = self.app.diskIo 457 | path = self.getTestPath(path) 458 | diskIo.setReadOnly(path, readOnly) 459 | 460 | def assertFileContents(self, path, expectedContents): 461 | diskIo = self.app.diskIo 462 | path = self.getTestPath(path) 463 | actualContents = diskIo.getFileContents(path) 464 | self.assertEqual(actualContents, expectedContents) 465 | 466 | def assertDirExists(self, path): 467 | diskIo = self.app.diskIo 468 | path = self.getTestPath(path) 469 | self.assertTrue(diskIo.dirExists(path)) 470 | 471 | def assertDirEmpty(self, path): 472 | diskIo = self.app.diskIo 473 | path = self.getTestPath(path) 474 | self.assertTrue(diskIo.isDirEmpty(path)) 475 | 476 | def assertPathDoesNotExist(self, path): 477 | diskIo = self.app.diskIo 478 | path = self.getTestPath(path) 479 | self.assertFalse(diskIo.anyExists(path)) 480 | -------------------------------------------------------------------------------- /vimswitch/test/test_ZipFile.py: -------------------------------------------------------------------------------- 1 | from .FileSystemTestCase import FileSystemTestCase 2 | from vimswitch.DiskIo import DiskIo 3 | from zipfile import ZipFile 4 | 5 | 6 | class TestZipFile(FileSystemTestCase): 7 | def setUp(self): 8 | FileSystemTestCase.setUp(self) 9 | self.diskIo = DiskIo() 10 | 11 | def test_extract_extractsFile(self): 12 | zipPath = self.getDataPath('simple.zip') 13 | destPath = self.getTestPath('') 14 | destFile1Path = self.getTestPath('file1.txt') 15 | destFile2Path = self.getTestPath('file2.txt') 16 | zipFile = ZipFile(zipPath) 17 | 18 | zipFile.extractall(destPath) 19 | 20 | file1Expected = 'test data' 21 | file1Actual = self.diskIo.getFileContents(destFile1Path) 22 | file2Expected = 'test data 2' 23 | file2Actual = self.diskIo.getFileContents(destFile2Path) 24 | self.assertTrue(file1Actual, file1Expected) 25 | self.assertTrue(file2Actual, file2Expected) 26 | -------------------------------------------------------------------------------- /vimswitch/test/workingDir/README: -------------------------------------------------------------------------------- 1 | Use `git update-index --skip-worktree README` to ignore this file. 2 | 3 | This directory is used as a temporary working area for tests that work on 4 | files. 5 | 6 | Everything in this directory gets deleted before and after a test run. 7 | Including this file. But we still need this file in git because git refuses to 8 | add empty folders. -------------------------------------------------------------------------------- /vimswitch/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1' 2 | --------------------------------------------------------------------------------