├── .freenode ├── .gitignore ├── MANIFEST.in ├── README.markdown ├── bin ├── __pin ├── __pincomp └── pin.sh ├── pin ├── __init__.py ├── command.py ├── config.py ├── delegate.py ├── env.py ├── event.py ├── hook.py ├── plugins │ ├── __init__.py │ └── core.py ├── registry.py └── util.py ├── requirements.txt ├── scripts └── pin ├── settings.yml ├── setup.py └── tests ├── test_config.py ├── test_events.py ├── test_registry.py └── test_utils.py /.freenode: -------------------------------------------------------------------------------- 1 | 32r9fjnjm3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py 2 | include requirements.txt 3 | include README.markdown 4 | include scripts/pin 5 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | pin 2 | ====== 3 | 4 | **pin** is a plugin-based command-line utility that helps you manage your software development projects. At it's core, it is a registry of where your projects reside on your file-system. Registering your project with **pin** lets you use utilize the various plugins. Since **pin** is generic, what this means exactly is based on what your project is and what plugins you have installed. 5 | 6 | ### Installation 7 | 8 | $ sudo pip install pin 9 | 10 | ### Usage 11 | 12 | To use **pin** you will need to source it's shell-script which is installed under the name **pin.sh**. You may want to add this to your **~/.bashrc**: 13 | 14 | $ source pin.sh 15 | 16 | The **pin** command will now be available to you. To see the core pin commands you can use the **help** command: 17 | 18 | $ pin help 19 | usage: pin [-v] subcommand 20 | 21 | positional arguments: 22 | subcommand any subcommand available below 23 | 24 | optional arguments: 25 | -v, --version 26 | Available commands for /home/dlacewell: 27 | pin go [project] 28 | - Teleport to a specific project. 29 | pin help [-a] [command [subcommand]] 30 | - This help information. 31 | pin init 32 | - Initialize pin in the current directory. 33 | $ 34 | 35 | 36 | ### Initialization 37 | 38 | Lets try out **pin init** in a new directory: 39 | 40 | $ cd; mkdir tmp 41 | $ cd tmp/ 42 | $ pin init 43 | Creating .pin directory structure... 44 | pin project initialized in: /home/dlacewell/tmp/ 45 | $ 46 | 47 | **pin** has created a project directory located at */home/dlacewell/tmp/.pin/* **Generally, commands that operate upon your project can be used *anywhere* below the project's root directory**. You'll now notice that if we execute the help command once more the **init** command has been replaced by the **destroy** command. This feature of command relevancy is pretty handy. Depending on whether or not you're in a project or what kinds of tools (like fabric or paver) your project uses will affect what commands are available to you. 48 | 49 | $ pin help 50 | usage: pin [-v] subcommand 51 | 52 | positional arguments: 53 | subcommand any subcommand available below 54 | 55 | optional arguments: 56 | -v, --version 57 | Available commands for /home/dlacewell/tmp: 58 | pin destroy 59 | - Destroy and unregister the project from pin. 60 | pin go [project] 61 | - Teleport to a specific project. 62 | pin help [-a] [command [subcommand]] 63 | - This help information. 64 | 65 | You can always pass the *-a* or *--all* option to help to see a list of all commands that **pin** knows about. However, do not expect irrelevant commands to do anything meaningful if you try to use them: 66 | 67 | $ pin help -a 68 | usage: pin [-v] subcommand 69 | 70 | positional arguments: 71 | subcommand any subcommand available below 72 | 73 | optional arguments: 74 | -v, --version 75 | Available commands for /home/dlacewell/tmp: 76 | pin destroy 77 | - Destroy and unregister the project from pin. 78 | pin go [project] 79 | - Teleport to a specific project. 80 | pin help [-a] [command [subcommand]] 81 | - This help information. 82 | pin init [--venv] [--pip] [--autoenv] 83 | - Initialize pin in the current directory. 84 | 85 | ### Core Commands 86 | 87 | **pin init** : Initializes the .pin directory and registers the path with ~/.pinconf/registry.yml 88 | 89 | **pin destroy** : Deletes the project's .pin directory and unregisters the project path. Only works from inside a project tree. 90 | 91 | **pin go ** : Teleports to the project root if a name is provided. If no name is provided a menu will be presented. 92 | 93 | **pin help** : Lists all pin commands including any provided by installed plugins. 94 | 95 | ## Plugin Support 96 | 97 | **pin** doesn't do much on it's own but plugins can add new functionality to existing commands or new commands all together. Let's go ahead and install the *pin-venv* plugins to give **pin** the ability to work with *VirtualEnv*. 98 | 99 | Remove existing pin dot-folder and install pinvenv 100 | 101 | $ rm -fdr .pin/ 102 | $ sudo pip install pinvenv 103 | ... 104 | 105 | Notice that the init command now supports the --venv option 106 | 107 | $ pin help 108 | usage: pin [-v] subcommand 109 | 110 | positional arguments: 111 | subcommand any subcommand available below 112 | 113 | optional arguments: 114 | -v, --version 115 | Available commands for /home/dlacewell/tmp: 116 | pin go [project] 117 | - Teleport to a specific project. 118 | pin help [-a] [command [command ...]] 119 | - This help information. 120 | pin init [--venv] [--autoenv] 121 | - Initialize pin in the current directory. 122 | 123 | 124 | Reinitalize with VirtualEnv support 125 | 126 | $ pin init --venv 127 | Creating .pin directory structure... 128 | Creating virtualenv... 129 | pin project initialized in: /home/dlacewell/tmp 130 | $ ls .pin/env 131 | bin include lib 132 | $ 133 | 134 | ### Get Plugins 135 | 136 | Plugins to extend pin's core functionality can be found at the [Pin Cushion](https://github.com/dustinlacewell/pin/wiki/Pin-Cushion) 137 | 138 | ### Write Plugins 139 | 140 | Plugins for **pin** are packaged as Namespace packages. Ensure that your plugin package resembles the following structure: 141 | 142 | yourpackage/ 143 | setup.py 144 | requirements.tx 145 | README 146 | pin/ 147 | __init__.py 148 | plugins/ 149 | __init__.py 150 | yourpackage.py 151 | 152 | To make your package namespaced you will need to add the following lines to each of the two **__init__.py** files: 153 | 154 | import pkg_resources 155 | pkg_resources.declare_namespace(__name__) 156 | 157 | The two plugin-classes that you can register with pip are **commands** and **hooks**. Before covering those specifically, let's review some notable API available for plugins to use: 158 | 159 | ### Utility API 160 | 161 | * **util.path_has_project(path)** : Determine if the supplied path contains the pin project-directory. 162 | 163 | * **util.get_project_root(path)** : Find the root project directory for the path, if there is one. 164 | 165 | * **util.get_settings_filename()** : Get the absolute path to the pin settings YAML file 166 | 167 | * **util.get_registry_filename()** : Get the absolute path to the pin registry YAML file 168 | 169 | 170 | ### Writing Commands 171 | 172 | The base command class is **command.PinCommand**. Your command will be a subclass that you register with **command.register(cls)**. There are a number of methods that you can override to define the behavior of your command. At minimum your class needs to define a class-attribute '**command**' which is the name of your command. Let's write a simple command called '*check*' the determines if the current-working-directory is inside of a pin project: 173 | 174 | class CheckCommand(command.PinCommand): 175 | command = 'check' 176 | 177 | Just to illustrate the proper way to handle arguments we'll support an optional path argument to check for paths other than the current-working-directory. Arguments are processed via an ArgumentParser and each command is automatically provided a parser to use. In addition to the parser each command is provided a few data attributes. Here is the **PinCommand** initializer method: 178 | 179 | def __init__(self, args): 180 | self.cwd = os.getcwd() 181 | self.root = get_project_root(self.cwd) 182 | self.args = args 183 | self.parser = self._getparser() 184 | self.options = self._getoptions(args) 185 | 186 | You can see that the command recieves the current-working-directory, the project root directory (if there is one), any arguments provided to the command, the ArgumentParser and the Options object returned by the parser. For the parser and options, PinCommand provides the **PinCommand.setup_parser()** method that you can override in order to configure your command's arguments. Let's setup an optional path argument now: 187 | 188 | class CheckCommand(command.PinCommand): 189 | command = 'check' 190 | 191 | def setup_parser(self, parser): 192 | parser.add_argument('path', nargs='?', default=self.cwd) 193 | 194 | That's all we have to do to add the optional path argument. Now, either the user supplied path or the current-working-directory will end up as an attribute; specifically **self.options.path**. We can use this data in the **PinCommand.execute()** method to do our check. 195 | 196 | from pin.util import get_project_root 197 | 198 | class CheckCommand(command.PinCommand): 199 | command = 'check' 200 | 201 | def setup_parser(self, parser): 202 | parser.add_argument('path', nargs='?', default=self.cwd) 203 | 204 | def execute(self): 205 | root = get_project_root(self.options.path) 206 | if root: 207 | print "The path is a part of the project at:", root 208 | else: 209 | print "The path is not part of a pin project." 210 | 211 | **get_project_root** takes a path and walks up through the parents checking for a **.pin** directory. If it finds it, it will return that path. This is how we know if we're under a project tree. 212 | 213 | ### User Methods 214 | 215 | In addition to **setup_parser** and **execute** there are a few other methods worth mentioning: 216 | 217 | #### is_relevant 218 | 219 | The **is_relevant** method is called for each command in various places such as the built in help command to determine what commands to show help for. That way the user is not encouraged, for example to attempt to reinitalize pin in an existing pin project or subdirectory of one (even though it wouldn't work anyway.) 220 | 221 | **is_relevant** returns True by default but you can use your own logic to determine if you command should be available: 222 | 223 | class PinInitCommand(command.PinCommand): 224 | '''Initialize pin in the current directory.''' 225 | 226 | command = 'init' 227 | 228 | def is_relevant(self): 229 | return not self.root 230 | 231 | #### setup_parser 232 | 233 | As mentioned above, **setup_parser** can be used to define the arguments for your command. If you've used ArgumentParser before you'll be comfortable adding arguments of various types. If not, you'll want to check the Argparse documentation. You can also use **setup_parser** to configure the parser in other ways, such as setting your command's usage or help. However setting the usage is discouraged as that will prevent any dynamically added arguments, from hooks, from appearing in your command's help. More on that later. 234 | 235 | class PinGoCommand(command.PinCommand): 236 | ''' 237 | Teleport to a specific project. 238 | ''' 239 | command = 'go' 240 | 241 | def setup_parser(self, parser): 242 | parser.add_argument('project', nargs="?") 243 | 244 | #### write_script 245 | 246 | The easiest way to explain the **write_script** method is with an example. The built in **`go'** command: 247 | 248 | class PinGoCommand(command.PinCommand): 249 | ''' 250 | Teleport to a specific project. 251 | ''' 252 | command = 'go' 253 | 254 | def setup_parser(self, parser): 255 | parser.add_argument('project', nargs="?") 256 | 257 | def execute(self): 258 | self.path = registry.pathfor(self.options.project) 259 | return self.path 260 | 261 | def write_script(self, file): 262 | if self.path: 263 | file.write("cd %s\n" % self.path) 264 | 265 | command.register(PinGoCommand) 266 | 267 | 268 | The **PinGoCommand** takes a single optional argument **`project'**. When the command executes, it asks the **registry** module for the absolute path of the project containing the word the user supplied. **write_script** is run after the command has successfully executed. **PinGoCommand** uses the supplied **file** object to write a shell statement telling the user's shell to change directories to the previously looked up project path. Use **write_script** if you need to write a command that should affect the user's shell environment in this way. 269 | 270 | 271 | #### execute 272 | 273 | It should be fairly obvious that **execute** is where you should put the work of your command. One thing to mention though is that your command is only considered to have successfully executed if **execute** returns True. If you do not return a *truth-y* value, **done** and **write_script** will not be called. 274 | 275 | def execute(self): 276 | if self.root: 277 | self.raise_exists() 278 | else: 279 | print "Creating .pin directory structure..." 280 | registry.initialize_project(self.cwd) 281 | return True 282 | 283 | 284 | #### done 285 | 286 | **done**'s utility may be questionable but it is there. This method will only be called if your command's **execute** method returns a truthful value. So I guess it can be considered a conveinence for that condition. 287 | 288 | # from PinDestroyCommand 289 | def done(self): 290 | print "Pin project has been destroyed." 291 | 292 | 293 | ### Delegate Commands 294 | 295 | **PinDelegateCommand** is a **PinCommand** that supports subcommands of various sorts. Delegate commands can also have functionality of their own. The subcommands can either be namespaced PinCommands or the subcommands can be dynamically interpreted depending on what you're trying to do. 296 | 297 | The basic **PinDelegateCommand** is one that comes with a number **namespaced PinCommands**. In the context of Pin, namespaced commands simply means that the **command name** is *prefixed* with the name of the parent command. Let's take a look at Pin's pip plugin, pin-pip. 298 | 299 | class PinPipCommand(command.PinDelegateCommand): 300 | ''' 301 | Commands for managing dependencies with pip. 302 | ''' 303 | command = 'pip' 304 | subcommands = [PinPipMeetCommand, PinPipRequiresCommand] 305 | 306 | def is_relevant(self): 307 | return self.root and \ 308 | os.path.isfile(os.path.join(self.root, 'requirements.txt')) 309 | 310 | def setup_parser(self, parser): 311 | parser.usage = "pin pip [subcommand]" 312 | 313 | command.register(PinPipCommand) 314 | 315 | **PinPipCommand** only implements two methods. The **is_relevant** method merely checks to see if there is a **requirements.txt** file in the project root. The **setup_parser** method simply hardcodes the command's usage string. You'll notice that there is a class attribute **subcommands** which lists two other classes. Let's take a look at **PinPipRequiresCommand**: 316 | 317 | class PinPipRequiresCommand(command.PinSubCommand): 318 | '''Print project's requirements.txt file''' 319 | 320 | command = 'pip-requires' 321 | 322 | def setup_parser(self, parser): 323 | parser.usage = "pin pip requires" 324 | 325 | def execute(self): 326 | self.script = '' 327 | requirements_file = os.path.join(self.root, 'requirements.txt') 328 | if os.path.isfile(requirements_file): 329 | self.script = "cat %s;" % requirements_file 330 | return True 331 | 332 | def write_script(self, file): 333 | file.write(self.script) 334 | 335 | command.register(PinPipRequiresCommand) 336 | 337 | The **pip-requires** subcommand is also quite simple. The **setup_parser** method hardcodes the usage string. The **execute** method determines the project's **requirements.txt** file's absolute path and generates a one line shell script to **cat** out the contents of the file. Finally, the **write_script** method writes out the shell script into the file object resulting in the **requirements.txt** file to be displayed on the user's screen. 338 | 339 | The important thing to note here is the name of the command, **pip-requires**. This is the namespacing bit we mentioned before. Namespacing the command in this way, by prefixing the name of the parent command, will ensure that **requires** is only available behind its parent, in this case **pip**. 340 | 341 | dlacewell@scarf:pin(master)$ pin requires 342 | usage: pin [-v] subcommand 343 | 344 | positional arguments: 345 | subcommand any subcommand available below 346 | 347 | optional arguments: 348 | -v, --version 349 | Available commands for /home/dlacewell/dev/mine/pin: 350 | (...cont) 351 | 352 | 353 | dlacewell@scarf:pin(master)$ pin pip requires 354 | PyYAML>=3.09 355 | argparse>=1.2.1 356 | straight.plugin>=1.0 357 | 358 | 359 | The raw name may also be used: 360 | 361 | dlacewell@scarf:pin(master)$ pin pip-requires 362 | PyYAML>=3.09 363 | argparse>=1.2.1 364 | straight.plugin>=1.0 365 | 366 | To demonstrate delegate commands that dynamically interpret its subcommands, lets walk through Pin's Paver plugin pin-paver. If you're not familiar with Paver it is a commandline utility that allows you to easily invoke methods inside of your project's **pavement.py** file. These methods usually do work involving things like building documentation, packaging and things like that. 367 | 368 | class PinPaverCommand(command.PinDelegateCommand): 369 | '''Commands inside your pavement file. 370 | ''' 371 | command = 'paver' 372 | 373 | def is_relevant(self): 374 | return self.root and \ 375 | os.path.isfile(os.path.join(self.root, 'pavement.py')) 376 | 377 | The **PinPaverCommand** starts off by defining its **is_relevant** method which only returns true if it can find a **pavement.py** file in the root of the Pin project. 378 | 379 | def setup_parser(self, parser): 380 | parser.usage = "pin paver [subcommand [args ..]]" 381 | parser.description = "Access commands within your pavement file" 382 | 383 | 384 | The parser setup simply adds a dummy usage description to inform the user that the paver command takes subcommands. When we do **pin help paver** we see that the help includes the commands inside my **pavement.py** file: 385 | 386 | dlacewell@scarf:~/dev/mine/pin$ pin help paver 387 | usage: pin paver [subcommand [args ..]] 388 | 389 | Access commands within your pavement file 390 | 391 | positional arguments: 392 | subcommand 393 | sdist - Generate docs and source distribution. 394 | 395 | The way that **PinPaverCommand** informs Pin what subcommands are available it, is by implementing the **get_commands** method. **get_commands** returns a dictionary who's keys are the available commands. What values your dictionary keys map to isn't currently important as the values are unused. In the case of **PinPaverCommand**, this involves importing your **pavement.py** file and asking paver for a list of the tasks within: 396 | 397 | def get_subcommands(cls): 398 | cwd = os.getcwd() 399 | root = get_project_root(cwd) 400 | try: 401 | sys.path.append(root) 402 | mod = __import__('pavement') 403 | env = Environment(mod) 404 | tasks = env.get_tasks() 405 | maxlen, tasklist = _group_by_module(tasks) 406 | for name, group in tasklist: 407 | if name == 'pavement': 408 | return dict((t.shortname, t) for t in group) 409 | except ImportError, e: 410 | return dict() 411 | 412 | To execute the specified paver command, the paver module's regular api is used: 413 | 414 | def execute(self): 415 | if self.root: 416 | os.chdir(self.root) 417 | main(self.options.subcommand) 418 | return True 419 | 420 | 421 | ## PinHooks 422 | 423 | Finally, we will take a look at Pin's last plugin class, **PinHook**. A good example of which is **PipPinHook** which tracks the creation of a new Pin project, processing any Pip requirements all starting from a new argument that grants the **`pin init'** command. 424 | 425 | The PinHook starts out similarly to the PinCommand family. In particular, you'll notice the **`name'** class-attribute. Since PinHooks are managed seperately than PinCommands they have their own namespace so the name "pip" here is okay: 426 | 427 | from pin.event import eventhook 428 | from pin.hook import PinHook, register 429 | 430 | class PipPinHook(PinHook): 431 | ''' 432 | Processes a requirements.txt file with pip 433 | ''' 434 | name = "pip" 435 | 436 | def __init__(self): 437 | self.options = None 438 | 439 | 440 | ### Adding arguments 441 | 442 | Another new thing you may have noticed was that we imported **eventhook** from **pin.event**. This is a decorator and you need to pass the name of an event to it, like **'init-post-parser'**. The first part is the name of the command you want to hook, in this case **'init'**. The rest is the name of the event you want to hook, here **'post-parser'**. The function that you decorate will be called anytime the specified command triggers the right event. Check the next section for information on the standard events. 443 | 444 | Observe how **PipPinHook** adds itself as a new argument to **Init**. After init calls its own **setup_parser** method, this post_parser method will be called with init's parser to be modified. Here a single argument is added **--pip**.: 445 | 446 | @eventhook('init-post-parser') 447 | def init_post_parser(self, parser): 448 | parser.add_argument('--pip', action='store_true') 449 | 450 | Here, post_args is called after init has finished processing the commandline input through the ArgumentParser. Both the original arguments and the resulting options object are passed in. In this case, we simply save the options for later. 451 | 452 | @eventhook('init-post-args') 453 | # parse --pip flag 454 | def init_post_args(self, args, options): 455 | self.options = options 456 | 457 | Now we'll wait to see if init successfully executes and if it does we'll save the path to the root of the new pin project for later: 458 | 459 | @eventhook('init-post-exec') 460 | # save project root 461 | def init_post_exec(self, cwd, root): 462 | self.options.root = cwd 463 | 464 | Next, instead of hooking another init event, we hook another plugin, pip-venv - Pip's VirtualEnv support. The **post-create** event will pass us the path to the newly created VirtualEnv. If both our **--pip** and **--venv** options are present then we store this path to the options object for the next step: 465 | 466 | @eventhook('venv-post-create') 467 | def venv_post_create(self, path): 468 | # only install if options were present 469 | if self.active and self.options.venv: 470 | self.options.venvpath = path 471 | 472 | 473 | Lastly, we add our own bit of shell-script to invoke the external pip command, telling it to install our requirements.txt file using the new VirtualEnv. 474 | 475 | @eventhook('init-post-script') 476 | # install the requirements file 477 | def init_post_script(self, file): 478 | if self.options.venvpath: 479 | venvopt = "-E %s" % self.options.venvpath 480 | file.write("pip install %s -r %s;" % (venvopt, 481 | os.path.join(self.options.root, 482 | 'requirements.txt'))) 483 | 484 | register(PipPinHook) 485 | 486 | 487 | ## Standard Events 488 | 489 | - pre-parser : Before a command configures its ArgumentParser 490 | + parameters: 491 | -- parser : The ArgumentParser 492 | 493 | - post-parser : After a command configures its ArgumentParser 494 | + parameters: 495 | -- parser : The ArgumentParser 496 | 497 | - pre-args : Before a command parses its argument input 498 | + parameters: 499 | -- args : The user supplied argument input 500 | 501 | - post-args : After a command parses its argument input 502 | + parameters: 503 | -- args : The user supplied argument input 504 | -- options : The resulting parsed arguments object 505 | 506 | - pre-script : Before a command writes out its shell-script 507 | + parameters: 508 | -- file : The open file object representing the shell-script 509 | 510 | - post-script : After a command writes out its shell-script 511 | + parameters: 512 | -- file : The open file object representing the shell-script 513 | 514 | - pre-exec : Before a command performs its actual work 515 | + parameters: 516 | -- cwd : The current directory where the command was performed 517 | -- root : The path of the parent root pin project directory, if there is one 518 | 519 | - post-exec : After a command has performed its actual work 520 | + parameters: 521 | -- cwd : The current directory where the command was performed 522 | -- root : The path of the parent root pin project directory, if there is one 523 | 524 | 525 | 526 | 527 | -------------------------------------------------------------------------------- /bin/__pin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if "__main__" == __name__: 4 | from pin.delegate import PinDelegator 5 | from pin.registry import establish_settings_folder, create_registry, load_registry 6 | establish_settings_folder() 7 | create_registry() 8 | load_registry() 9 | delegate = PinDelegator() 10 | 11 | -------------------------------------------------------------------------------- /bin/__pincomp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pin.util import compgen 4 | print compgen() 5 | 6 | -------------------------------------------------------------------------------- /bin/pin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | pin () { 4 | __pin "$@" 5 | __sourceit 6 | } 7 | 8 | 9 | __sourceit () { 10 | SOURCEFILE=~/.pinconf/source.sh 11 | if [ -f "$SOURCEFILE" ] 12 | then 13 | eval `cat $SOURCEFILE` 14 | fi 15 | } -------------------------------------------------------------------------------- /pin/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.1dev' 2 | PROJECT_FOLDER = '.pin' 3 | SETTINGS_ROOT = '~' 4 | SETTINGS_FOLDER = '.pinconf' 5 | SETTINGS_FILE = 'settings.yml' 6 | REGISTRY_FILE = 'registry.yml' 7 | SHELL_FILE = 'source.sh' 8 | 9 | 10 | def load_plugins(): 11 | from straight.plugin import load 12 | load("pin.plugins") 13 | -------------------------------------------------------------------------------- /pin/command.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from argparse import ArgumentParser 3 | 4 | from pin import event 5 | from pin.util import get_project_root 6 | 7 | _commands = {} 8 | 9 | def register(cls): 10 | '''Register class as available command''' 11 | _commands[cls.command] = cls 12 | 13 | def get(name): 14 | '''Get command-class by name''' 15 | return _commands.get(name, None) 16 | 17 | def all(): 18 | '''Return a dictionary of all available commands''' 19 | return _commands 20 | 21 | class PinCommand(object): 22 | ''' 23 | A Pin command 24 | 25 | PinCommands represent actions that can be performed. Commands may 26 | fire any number of events including the standard events listed below. 27 | 28 | attributes: 29 | cwd : path where command was executed 30 | root : root path of pin-project if one exists 31 | args : list of passed command arguments 32 | parser : command specific argparse.ArgumentParser 33 | options : result of argument parsing 34 | 35 | Hookable events allow PinHooks to respond to or modify the behavior 36 | of PinCommands. Plugins may radically change the way other plugin or 37 | even standard commands work. 38 | 39 | Each event has a pre and post version. 40 | For example, the `exec' event may be hooked by `pre-exec' or `post-exec' 41 | depending on what you want to do. 42 | 43 | Some hooks are passed valuable data related to the event that may be 44 | modified to change the behavior of the event. 45 | 46 | standard hookable events: 47 | `parser' : 48 | -- When command configures its argument parser 49 | -- Args : 50 | parser - the ArgumentParser 51 | 52 | `args' : 53 | -- When argument parsing takes place 54 | -- Args: 55 | args - User supplied arguments to the command 56 | 57 | `script' : 58 | -- As the external sourcing script is generated 59 | -- Args: 60 | file - File object representing the script to be generated 61 | 62 | `exec' : 63 | -- When the command executes its work 64 | -- Args: 65 | cwd - the directory path the command was executed in 66 | root - the root path of the pin project, if one exists 67 | 68 | overridable methods: 69 | is_relevant, setup_parser, write_script, execute, done 70 | ''' 71 | 72 | command = None 73 | 74 | def __init__(self, args): 75 | # get current working directory 76 | self.cwd = os.getcwd() 77 | # find out if this is a pin project 78 | self.root = get_project_root(self.cwd) 79 | self.args = args 80 | # get command's argument-parser 81 | self.parser = self._getparser() 82 | # get command's parsed arguments 83 | self.options = self._getoptions(args) 84 | 85 | def fire(self, name, *args, **kwargs): 86 | ''' 87 | Fire an arbitrary event 88 | 89 | Event names, by convention, should be lower-case-and-hypen-seperated. 90 | When events are hooked, the command that emitted the event will 91 | have its name prepended in this way. 92 | 93 | For example, before the InitCommand executes it will fire an event 94 | that can be handled by the name 'init-pre-exec' for an event fired 95 | with the name 'pre-exec'. 96 | ''' 97 | event.fire(self.command + '-' + name, *args, **kwargs) 98 | 99 | def _getparser(self): 100 | parser = ArgumentParser(prog='pin ' + self.command, add_help=False) 101 | if self.__doc__: 102 | parser.description = self.__doc__.splitlines()[0] 103 | self.fire('pre-parser', parser) 104 | self.setup_parser(parser) 105 | self.fire('post-parser', parser) 106 | return parser 107 | 108 | def _getoptions(self, args): 109 | self.fire('pre-args', args) 110 | options, extargs = self.parser.parse_known_args(args) 111 | self.fire('post-args', extargs, options) 112 | return options 113 | 114 | def _writescript(self): 115 | with open(os.path.expanduser("~/.pinconf/source.sh"), 'w') as file: 116 | self.fire('pre-script', file) 117 | self.write_script(file) 118 | self.fire('post-script', file) 119 | 120 | def _execute(self): 121 | self.fire('pre-exec', self.cwd, self.root) 122 | success = self.execute() 123 | if success: 124 | self.fire('post-exec', self.cwd, self.root) 125 | self._writescript() 126 | self.done() 127 | 128 | def is_relevant(self): 129 | ''' 130 | Determines whether or not the command is visible in the current context. 131 | ''' 132 | return True 133 | 134 | def setup_parser(self, parser): 135 | ''' 136 | User overridable method for configuring the command's ArgumentParser. 137 | ''' 138 | pass 139 | 140 | def write_script(self, file): 141 | ''' 142 | User overridable method for writing out any nessecary post-execution 143 | bash code. 144 | ''' 145 | pass 146 | 147 | def execute(self): 148 | ''' 149 | User overridable method for implementing the actual work of the command. 150 | ''' 151 | pass 152 | 153 | def done(self): 154 | ''' 155 | User overridable method for work done after command has completed. 156 | ''' 157 | pass 158 | 159 | 160 | class PinSubCommand(PinCommand): 161 | def is_relevant(self): 162 | return False 163 | 164 | class PinDelegateCommand(PinCommand): 165 | subcommands = [] # handled commands 166 | 167 | def _getparser(self): 168 | ''' 169 | Adds an implicit 'subcommand' argument. 170 | ''' 171 | parser = ArgumentParser(prog='pin ' + self.command, add_help=False) 172 | parser.add_argument('subcommand', nargs='*') 173 | self.fire('pre-parser', parser) 174 | self.setup_parser(parser) 175 | self.fire('post-parser', parser) 176 | return parser 177 | 178 | def _execute(self): 179 | success = False 180 | if self.options.subcommand: 181 | for subcom in self.subcommands: 182 | if subcom.command == '-'.join((self.command, self.options.subcommand[0])): 183 | subcom(self.args[1:])._execute() 184 | return 185 | self.fire('pre-exec', self.cwd, self.root) 186 | success = self.execute() 187 | else: 188 | self.fire('pre-exec', self.cwd, self.root) 189 | success = self.execute() 190 | if success: 191 | self.fire('post-exec', self.cwd, self.root) 192 | self._writescript() 193 | self.done() 194 | 195 | @classmethod 196 | def get_subcommands(cls): 197 | return dict((c.command, c) for c in cls.subcommands) 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /pin/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from yaml import load, dump 4 | try: 5 | from yaml import CLoader as Loader 6 | from yaml import CDumper as Dumper 7 | except ImportError: 8 | from yaml import Loader, Dumper 9 | 10 | from pin import SETTINGS_FILE, REGISTRY_FILE 11 | from pin.util import get_project_root 12 | 13 | GLOBAL_PATH = os.path.normpath(os.path.expanduser("~/.pinconf")) 14 | DEFAULT_CONFIG = {} 15 | 16 | def merge(user, default): 17 | if isinstance(user,dict) and isinstance(default,dict): 18 | for k,v in default.iteritems(): 19 | if k not in user: 20 | user[k] = v 21 | else: 22 | user[k] = merge(user[k],v) 23 | return user 24 | 25 | def load_yaml(path): 26 | config_file = open(path, 'r+') 27 | config = load(config_file, Loader=Loader) 28 | config_file.close() 29 | return config 30 | 31 | def save_yaml(path, config): 32 | config_file = open(path, 'w') 33 | config_file.write(dump(config, default_flow_style=False)) 34 | config_file.close() 35 | 36 | def check_configuration(): 37 | if not os.path.isdir(GLOBAL_PATH): 38 | os.mkdir(GLOBAL_PATH) 39 | configfile = os.path.join(GLOBAL_PATH, SETTINGS_FILE) 40 | registryfile = os.path.join(GLOBAL_PATH, REGISTRY_FILE) 41 | os.open(configfile, os.O_CREAT) 42 | os.open(registryfile, os.O_CREAT) 43 | 44 | 45 | def get_configuration(): 46 | check_configuration() 47 | config = DEFAULT_CONFIG 48 | config = merge(load_yaml(os.path.join(GLOBAL_PATH, SETTINGS_FILE)), config) 49 | # get project configuration 50 | root = get_project_root(os.getcwd()) 51 | if root: 52 | configfile = os.path.join(root, '.pin', SETTINGS_FILE) 53 | if os.path.isfile(configfile): 54 | config = merge(load_yaml(os.path.join(root, '.pin', SETTINGS_FILE)), config) 55 | return config 56 | 57 | config = get_configuration() 58 | -------------------------------------------------------------------------------- /pin/delegate.py: -------------------------------------------------------------------------------- 1 | import os 2 | from argparse import ArgumentParser 3 | 4 | from pin import load_plugins 5 | from pin import VERSION 6 | from pin import command, registry 7 | 8 | class CommandDelegator(object): 9 | parser = ArgumentParser(prog='pin', add_help=False) 10 | parser.add_argument('-v', '--version', 11 | action='version', version=VERSION) 12 | 13 | def __init__(self): 14 | self.delegate = False 15 | self.args, self.exargs = self.parser.parse_known_args() 16 | self.parser.add_argument('subcommand', nargs=1, default=None, 17 | help='any subcommand available below') 18 | 19 | if self.exargs: 20 | cmd, args = self.exargs[0], self.exargs[1:] 21 | else: 22 | cmd, args = 'help', None 23 | self.do_delegation(cmd, args) 24 | 25 | def do_default(self): 26 | self.parser.print_help() 27 | 28 | def do_delegation(self, cmd, args): 29 | raise NotImplementedError 30 | 31 | class PinDelegator(CommandDelegator): 32 | 33 | def do_delegation(self, cmd, args): 34 | load_plugins() 35 | # project precondition 36 | proj_path = registry.pathfor(cmd) 37 | if proj_path is not None: 38 | os.chdir(proj_path) 39 | cmd = args[0] 40 | args = args[1:] 41 | if '-' in cmd: 42 | cmd, newargs = cmd.split('-', 1) 43 | args = [newargs, ] + args 44 | comcls = command.get(cmd) or command.get('help') 45 | try: 46 | if comcls: 47 | comobj = comcls(args) 48 | if comobj.is_relevant(): 49 | comobj._execute() 50 | else: 51 | helpcls = command.get('help') 52 | helpobj = helpcls((cmd,)) 53 | helpobj._execute() 54 | print "\n'%s %s' command not relevant here." % (cmd, ' '.join(args).strip()) 55 | else: 56 | self.do_default() 57 | except KeyboardInterrupt: 58 | print "\n" 59 | pass 60 | -------------------------------------------------------------------------------- /pin/env.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinlacewell/pin/51e7ee7321379ad03d9431248567fd9caa338950/pin/env.py -------------------------------------------------------------------------------- /pin/event.py: -------------------------------------------------------------------------------- 1 | 2 | _events = {} 3 | 4 | def get_events(): 5 | return _events 6 | 7 | def register(name, callback): 8 | eventset = _events.get(name, set([])) 9 | eventset.add(callback) 10 | _events[name] = eventset 11 | 12 | def unregister(name, callback): 13 | eventset = _events.get(name) 14 | if eventset and callback in eventset: 15 | eventset.remove(callback) 16 | _events[name] = eventset 17 | 18 | def fire(name, *args, **kwargs): 19 | eventset = _events.get(name) 20 | if eventset: 21 | for handler in eventset: 22 | handler(*args, **kwargs) 23 | 24 | class eventhook(object): 25 | def __init__(self, eventname): 26 | self.eventname = eventname 27 | 28 | def __call__(self, f): 29 | def wrapped_f(*args, **kwargs): 30 | f(*args, **kwargs) 31 | wrapped_f.handled_event = self.eventname 32 | return wrapped_f 33 | -------------------------------------------------------------------------------- /pin/hook.py: -------------------------------------------------------------------------------- 1 | from pin import event 2 | 3 | _hooks = [] 4 | 5 | def register(hook): 6 | if hook not in _hooks: 7 | newhook = hook() 8 | newhook.register_hooks() 9 | _hooks.append(newhook) 10 | 11 | def unregister(hook): 12 | if hook in _hooks: 13 | _hooks.remove() 14 | 15 | class PinHook(object): 16 | 17 | name = None 18 | 19 | def __init__(self): 20 | pass 21 | 22 | def _isactive(self): 23 | return self.isactive() 24 | active = property(_isactive) 25 | 26 | def isactive(self): 27 | return True 28 | 29 | def fire(self, eventname, *args, **kwargs): 30 | event.fire(self.name + '-' + eventname, *args, **kwargs) 31 | 32 | def register_hooks(self): 33 | for attr in dir(self): 34 | attr = getattr(self, attr) 35 | eventname = getattr(attr, 'handled_event', None) 36 | if eventname: 37 | event.register(eventname, attr) 38 | 39 | -------------------------------------------------------------------------------- /pin/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinlacewell/pin/51e7ee7321379ad03d9431248567fd9caa338950/pin/plugins/__init__.py -------------------------------------------------------------------------------- /pin/plugins/core.py: -------------------------------------------------------------------------------- 1 | import os, shutil, pdb 2 | from argparse import ArgumentParser 3 | 4 | from pin import * 5 | from pin.util import * 6 | from pin.delegate import CommandDelegator 7 | from pin import command, registry 8 | 9 | class PinInitCommand(command.PinCommand): 10 | '''Initialize pin in the current directory.''' 11 | 12 | command = 'init' 13 | 14 | def is_relevant(self): 15 | return self.cwd != self.root 16 | 17 | def setup_parser(self, parser): 18 | parser.add_argument('--alias', nargs=1, default=None, metavar='alias') 19 | 20 | def raise_exists(self): 21 | msg = "Cannot initialize pin in an existing project: %s" % self.cwd 22 | print msg 23 | 24 | def execute(self): 25 | if self.cwd == self.root: 26 | self.raise_exists() 27 | else: 28 | print "Creating .pin directory structure..." 29 | alias = None 30 | if self.options.alias: 31 | alias = self.options.alias[0] 32 | registry.initialize_project(self.cwd, alias=alias) 33 | self.root = self.cwd 34 | return True 35 | 36 | def done(self): 37 | print "pin project initialized in: %s" % self.cwd 38 | command.register(PinInitCommand) 39 | 40 | 41 | 42 | class PinDestroyCommand(command.PinCommand): 43 | '''Destroy and unregister the project from pin.''' 44 | 45 | command = 'destroy' 46 | 47 | def is_relevant(self): 48 | return self.root 49 | 50 | def raise_no_project(self): 51 | msg = "No pin project found. (aborted)" 52 | print msg 53 | 54 | def execute(self): 55 | if not self.root: 56 | return self.raise_no_project() 57 | else: 58 | repeat = True 59 | while repeat: 60 | pinpath = os.path.join(self.root, PROJECT_FOLDER) 61 | print "WARNING: Will destory all data in the .pin directory!" 62 | os.system("ls %s" % pinpath) 63 | selection = option_select(['y', 'n'], "Really destroy?") 64 | if selection == 'n': 65 | print "Aborted." 66 | return 67 | elif selection == 'y': 68 | shutil.rmtree(pinpath) 69 | registry.unregister(self.root) 70 | return True 71 | def done(self): 72 | print "Pin project has been destroyed." 73 | 74 | command.register(PinDestroyCommand) 75 | 76 | class PinProjectProxy(object): 77 | '''Pin project project used for Go command''' 78 | 79 | def __init__(self, path): 80 | self.__doc__ = path 81 | 82 | 83 | class PinGoCommand(command.PinCommand): 84 | ''' 85 | Teleport to a specific project. 86 | ''' 87 | command = 'go' 88 | 89 | def setup_parser(self, parser): 90 | 91 | parser.add_argument('project', nargs="?") 92 | 93 | def execute(self): 94 | self.path = registry.pathfor(self.options.project, ask=True) 95 | if self.path: 96 | return self.path 97 | else: 98 | print "There are no registered pin projects." 99 | 100 | 101 | def write_script(self, file): 102 | if self.path: 103 | file.write("cd %s\n" % self.path) 104 | 105 | @classmethod 106 | def get_subcommands(cls): 107 | subs = dict() 108 | for p, meta in registry._projects.items(): 109 | alias = meta.get('alias') 110 | if alias: 111 | subs[alias] = PinProjectProxy(p) 112 | else: 113 | subs[os.path.basename(p)] = PinProjectProxy(p) 114 | return subs 115 | 116 | command.register(PinGoCommand) 117 | 118 | class PinAliasCommand(command.PinCommand): 119 | '''Manage project aliases''' 120 | 121 | command = 'alias' 122 | 123 | def setup_parser(self, parser): 124 | parser.add_argument('name', nargs="?") 125 | parser.add_argument('-p', '--proj', nargs="?", dest='project') 126 | 127 | 128 | def execute(self): 129 | # pdb.set_trace() 130 | name = self.options.name 131 | project = getattr(self.options, 'project', None) 132 | if project: 133 | proj_path = registry.pathfor(project) 134 | if not proj_path: 135 | print "* Project", project, "not found." 136 | return 137 | elif self.root: 138 | proj_path = self.root 139 | else: 140 | print "Must provide -p/--project option, if outside of pin project." 141 | return 142 | if name in registry._aliases: 143 | if proj_path != registry._aliases[name]: 144 | print "Alias `{alias}' already exists for other project:".format(alias=name) 145 | print registry._aliases[name] 146 | return 147 | else: 148 | print "Alias `{alias}' already set.".format(alias=name) 149 | return 150 | else: 151 | current_alias = registry._projects[proj_path].get('alias') 152 | if current_alias: 153 | prompt = "Alias `{alias}' already set, overwrite?".format(alias=current_alias) 154 | answer = option_select(['y','n'], prompt) 155 | if answer == 'n': 156 | print "Aborted." 157 | return 158 | 159 | _projects[proj_path]['alias'] = name 160 | _aliases[name] = proj_path 161 | registry.save_registry() 162 | print "Alias `{alias}' has been set for, {path}".format(alias=name, 163 | path=proj_path) 164 | return True 165 | 166 | command.register(PinAliasCommand) 167 | 168 | 169 | class PinHelpCommand(command.PinCommand): 170 | '''This help information.''' 171 | 172 | command = 'help' 173 | 174 | def setup_parser(self, parser): 175 | parser.add_argument('command', nargs='*', 176 | default=None) 177 | parser.add_argument('-a', '--all', dest='all', action='store_true') 178 | 179 | def process_simplecom(self, name, collen): 180 | comcls = command.get(name) 181 | comobj = comcls([]) 182 | if comobj.is_relevant() or self.options.all: 183 | usage = comobj.parser.format_usage().replace("usage: ", "") 184 | doc = comcls.__doc__ or '' 185 | print "{0: >{cl}} - {1: ^24}".format(usage, 186 | doc.strip(), 187 | cl=collen) 188 | 189 | def process_subcom(self, name, subcom, subcollen): 190 | doc = subcom.__doc__ or '' 191 | name = name.split('-')[-1] 192 | print " {0: >{scl}} - {1: ^24}".format(name, doc.strip(), scl=subcollen) 193 | 194 | def process_containercom(self, name, collen, subcollen): 195 | comcls = command.get(name) 196 | comobj = comcls([]) 197 | usage = comobj.parser.format_usage().replace("usage: ", "") 198 | print usage.strip() 199 | if hasattr(comcls, 'get_subcommands'): 200 | if comcls.__doc__: 201 | print "{0} {1}".format(' - ', comcls.__doc__.strip(), cl=collen) 202 | subcoms = comcls.get_subcommands() 203 | if subcoms: 204 | for name, subcom in subcoms.items(): 205 | self.process_subcom(name, subcom, subcollen) 206 | 207 | def do_default_help(self): 208 | ''' 209 | Render pin's general help 210 | 211 | This method will iterate through all available commands that declare 212 | themselves as being 'relevant'. Some processing is done to determine 213 | formatting widths of the output and help for delegate-commands 214 | that contain subcommands are dynamically computed. 215 | ''' 216 | # print generic pin help 217 | CommandDelegator.parser.print_help() 218 | comkeys = [k for k in command.all().keys() if '-' not in k] 219 | maxlength = len(max(comkeys, key=len)) 220 | simplekeys = [] # commands without subcommands 221 | containerkeys = [] # subcommand delgates 222 | subcomkeys = [] 223 | submaxlength = maxlength 224 | # iterate over all commands 225 | for k in comkeys: 226 | # get command class 227 | comcls = command.get(k) 228 | # get dummy instance 229 | comobj = comcls([]) 230 | # check if command is relevant 231 | if comobj.is_relevant() or self.options.all: 232 | # add to specific list based on `get_subcommands' attribute 233 | if hasattr(comcls, 'get_subcommands'): 234 | containerkeys.append(k) 235 | # grab all subcommand keys 236 | subcoms = comcls.get_subcommands() 237 | if subcoms: 238 | subcomkeys += subcoms.keys() 239 | else: 240 | simplekeys.append(k) 241 | # calculate global max-length of subcommand keys 242 | if subcomkeys: 243 | submaxlength = len(max(subcomkeys, key=len)) 244 | # sort all keys 245 | simplekeys.sort(); containerkeys.sort() 246 | if simplekeys or containerkeys: 247 | print "Available commands for %s:" % os.getcwd() 248 | # render simplekeys, then containerkeys 249 | for key in simplekeys: 250 | self.process_simplecom(key, maxlength) 251 | for key in containerkeys: 252 | self.process_containercom(key, maxlength, submaxlength) 253 | 254 | 255 | def execute(self): 256 | if self.options.command: 257 | clskey = '-'.join(self.options.command) 258 | comcls = command.get(clskey) 259 | if comcls: # if args point to pin command 260 | comobj = comcls([]) 261 | parser = comobj.parser 262 | parser.print_help() 263 | if hasattr(comcls, 'get_subcommands'): 264 | subcoms = comcls.get_subcommands() 265 | if subcoms: 266 | submaxlength = len(max(subcoms.keys(), key=len)) 267 | for name, subcom in subcoms.items(): 268 | self.process_subcom(name, subcom, submaxlength) 269 | else: # ask parent command 270 | pcomcls = command.get(self.options.command[0]) 271 | if pcomcls and hasattr(pcomcls, 'get_subcommands'): 272 | subcommands = pcomcls.get_subcommands() 273 | if subcommands: 274 | for name, com in subcommands.items(): 275 | if name == self.options.command[1]: 276 | print com.__doc__ or 'Command has no docstring.' 277 | else: 278 | self.do_default_help() 279 | 280 | 281 | 282 | else: 283 | self.do_default_help() 284 | 285 | command.register(PinHelpCommand) 286 | 287 | -------------------------------------------------------------------------------- /pin/registry.py: -------------------------------------------------------------------------------- 1 | import os, sys, pdb 2 | 3 | from yaml import load, dump 4 | try: 5 | from yaml import CLoader as Loader 6 | from yaml import CDumper as Dumper 7 | except ImportError: 8 | from yaml import Loader, Dumper 9 | 10 | 11 | from pin import * 12 | from pin.util import * 13 | 14 | def establish_settings_folder(): 15 | ''' 16 | Creates the pin user-settings folder if 17 | it does not exist. 18 | ''' 19 | path = get_settings_path() 20 | if not os.path.isdir(path): 21 | os.mkdir(path) 22 | with open(os.path.join(path, SETTINGS_FILE), 'w'): pass 23 | create_registry() 24 | 25 | def create_project_directory(path): 26 | project_path = os.path.join(path, PROJECT_FOLDER) 27 | os.mkdir(project_path) 28 | with open(os.path.join(project_path, SETTINGS_FILE), 'w'): pass 29 | 30 | def initialize_project(path, alias=None): 31 | create_project_directory(path) 32 | register(path, alias) 33 | 34 | # 35 | # # Registry 36 | # 37 | 38 | _projects = {} 39 | _aliases = {} 40 | 41 | def create_registry(): 42 | ''' 43 | Create the registry file if it does not 44 | already exist. 45 | ''' 46 | filename = get_registry_filename() 47 | if not os.path.isfile(filename): 48 | with open(filename, 'w') as file: 49 | file.write(dump(dict(projects={}))) 50 | 51 | def load_registry(): 52 | ''' 53 | Load the registry from disk. 54 | ''' 55 | global _projects, _aliases 56 | default = dict(projects={}) 57 | with open(get_registry_filename(), 'r') as file: 58 | _registry = load(file, Loader=Loader) or default 59 | _projects = _registry['projects'] 60 | for p, meta in _projects.iteritems(): 61 | alias = meta.get('alias') 62 | if alias: 63 | _aliases[alias] = p 64 | 65 | def get_registry(): 66 | return _projects 67 | 68 | def get_aliases(): 69 | return _aliases 70 | 71 | def save_registry(): 72 | ''' 73 | Save the registry to disk. 74 | ''' 75 | with open(get_registry_filename(), 'w') as file: 76 | file.write(dump(dict(projects=_projects))) 77 | 78 | def syncregistry(fin): 79 | def fout(*args, **kwargs): 80 | load_registry() 81 | ret = fin(*args, **kwargs) 82 | save_registry() 83 | return ret 84 | return fout 85 | 86 | @syncregistry 87 | def name_is_registered(name): 88 | ''' 89 | Returns whether a project name or alias 90 | is registered with pin. 91 | ''' 92 | return (name in _aliases 93 | or name in [os.path.basename(p) for p in _projects]) 94 | 95 | @syncregistry 96 | def path_is_registered(path): 97 | ''' 98 | Returns whether a project path 99 | is registered with pin. 100 | ''' 101 | return path in _projects 102 | 103 | @syncregistry 104 | def register(path, alias=None): 105 | ''' 106 | Register a project path with optional alias 107 | with pin. 108 | ''' 109 | if alias in _aliases: 110 | msg = "Alias, %s, already exists. Overwrite? [y/n] " % alias 111 | repeat = True 112 | while repeat: 113 | overwrite = raw_input(msg).strip().lower()[0] 114 | if overwrite in ['y', 'n']: 115 | repeat = False 116 | if overwrite == 'n': 117 | print "*** Alias ignored." 118 | alias = None 119 | 120 | if alias: 121 | _projects[path] = dict(alias=alias) 122 | _aliases[alias] = path 123 | else: 124 | _projects[path] = dict() 125 | 126 | @syncregistry 127 | def unregister(path): 128 | ''' 129 | Unregister a project path with pin. 130 | ''' 131 | if path in _projects: 132 | alias = _projects[path].get('alias') 133 | del _projects[path] 134 | if alias: 135 | del _aliases[alias] 136 | 137 | @syncregistry 138 | def pathfor(name, ask=False): 139 | ''' 140 | Returns the full path of a project by 141 | name or alias. If the name is not found 142 | None is returned. 143 | ''' 144 | # Check aliases first 145 | choices = [p for a, p in _aliases.items() if name and a.startswith(name)] 146 | # Enuemerate all projects in a folder `name` 147 | for p in _projects: 148 | if p not in choices: 149 | basename = os.path.basename(p) 150 | if os.path.basename(p).startswith(str(name)) or name is None: 151 | choices.append(p) 152 | n_choices = len(choices) 153 | if n_choices == 1: # return the only choice 154 | return choices[0] 155 | elif ask and n_choices > 0: 156 | # Get user to select choice 157 | return numeric_select(choices or _projects.keys(), 158 | "Select path", "Select path") 159 | 160 | -------------------------------------------------------------------------------- /pin/util.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | from pin import * 4 | 5 | 6 | def compgen(): 7 | '''Do command completion for bash.''' 8 | # requires plugins to be loaded 9 | load_plugins() 10 | from pin import command, registry 11 | 12 | # get argument information 13 | nargs, args = int(sys.argv[1]), sys.argv[2:] 14 | 15 | """ 16 | The offset here is a pointer to the argument being 17 | completed. Since pin allows you to call commands 18 | against remote projects we need to adjust the 19 | completion algorithm accordingly. Each time another 20 | level of indirection is added, the offset is 21 | increased. A visual example is appropriate here. 22 | 23 | The offset starts at 1: 24 | 25 | pin ini[tab] : off=1, nargs=1 - base command 26 | 27 | pin go mypro[tab] : off=1, nargs=2 - subcommand 28 | 29 | Indirections like help cause the offset to change: 30 | 31 | pin help destr[tab] : off=2, nargs=2 - base command 32 | 33 | pin help fab deplo[tab] : off=2, nargs=3 - subcommand 34 | 35 | So now the method of the algorithm is clear. If the 36 | offset is equal to the number of arguments, we know 37 | we need to complete a base command name. If the 38 | offset is one less than the number of arguments we 39 | know to complete a subcommand. Even with a project 40 | prefix this works: 41 | 42 | pin myproj help fab deplo[tab] : 43 | off = (1 + myproj + help) = 3 44 | nargs = 4 45 | = subcommand! 46 | """ 47 | off = 1 48 | proj_path = None 49 | # # # # # # # # 50 | # Project Name 51 | if args: 52 | proj_path = registry.pathfor(args[0]) 53 | # only increase the offset if we're not completing 54 | # the first argument. 55 | if nargs > 1 and proj_path is not None: 56 | # increase the offset 57 | off += 1 58 | # change to project directory 59 | os.chdir(proj_path) 60 | # # # # # # # # 61 | # Help command 62 | if args: 63 | # complete help command 64 | if nargs == off and "help".startswith(args[-1]): 65 | return 'help' 66 | # or increase offset by 1 67 | elif "help" in args: 68 | off += 1 69 | # # # # # # # # 70 | # base-command 71 | if nargs == off: 72 | arg = '' # default to empty arg 73 | if len(args) == off: 74 | # set working arg to item at offset 75 | arg = args[off-1] 76 | choices = " ".join([c for c in command._commands 77 | if c.startswith(arg) 78 | and command.get(c)([]).is_relevant()]) 79 | # return the choices if there are any 80 | if choices: 81 | return choices 82 | # we want commands to complete before 83 | # project names, so if we don't return any 84 | # choices above, complete a project name now 85 | # if we're completing the first argument 86 | if nargs == 1: 87 | if proj_path is not None: 88 | return os.path.basename(proj_path) 89 | # # # # # # # # 90 | # sub-commands 91 | elif nargs == off + 1: 92 | # get our parent command 93 | com = args[off-1] 94 | # get its class 95 | comcls = command.get(com) 96 | # if it is a delegate command 97 | if hasattr(comcls, 'get_subcommands'): 98 | # get partial subcommand name if user has typed anything 99 | # or else use empty string to complete all 100 | subcom = args[off] if len(args) == off+1 else '' 101 | # get a list of the parent command's subcommands 102 | subcom_choices = comcls.get_subcommands() 103 | # if there are any subcommands 104 | if subcom_choices: 105 | # clean subcommand names (they use command-subcommand format) 106 | choices = [k.split('-')[-1] for k in subcom_choices] 107 | # return the subcommands that start with the partial subcommand name 108 | return " ".join([c for c in choices if c.startswith(subcom)]) 109 | # return nothing 110 | return "" 111 | 112 | def walkup(path): 113 | ''' 114 | Walk through parent directories to root. 115 | ''' 116 | at_top = False 117 | while not at_top: 118 | yield path 119 | parent_path = os.path.normpath(os.path.join(path, "..")) 120 | if parent_path == path: 121 | at_top = True 122 | else: 123 | path = parent_path 124 | 125 | def name_from_path(path): 126 | return os.path.basename(path) 127 | 128 | def get_settings_path(): 129 | return os.path.expanduser(os.path.join(SETTINGS_ROOT, SETTINGS_FOLDER)) 130 | 131 | def path_has_project(path): 132 | ''' 133 | Determine if supplied path contains the pin project 134 | directory. 135 | ''' 136 | contents = os.listdir(path) 137 | if PROJECT_FOLDER in contents: 138 | return True 139 | 140 | def get_project_root(path): 141 | ''' 142 | Find the parent directory of the pin project, if 143 | there is one. 144 | ''' 145 | for directory in walkup(path): 146 | if path_has_project(directory): 147 | return directory 148 | 149 | def get_settings_filename(): 150 | return os.path.join(get_settings_path(), SETTINGS_FILE) 151 | 152 | def get_registry_filename(): 153 | return os.path.join(get_settings_path(), REGISTRY_FILE) 154 | 155 | def get_sourcing_filename(): 156 | return os.path.join(get_settings_path(), SHELL_FILE) 157 | 158 | def findroot(fin): 159 | def fout(path): 160 | path = get_project_root(path) 161 | fin(path) 162 | return fout 163 | 164 | 165 | def numeric_select(choices, prompt="Select from the above", title="Multiple choices possible:"): 166 | numeric_warning = False 167 | range_warning = False 168 | choices.sort() 169 | while True: 170 | print title 171 | for x in range(len(choices)): 172 | print " [%d] - %s" % (x + 1, str(choices[x])) 173 | if numeric_warning: 174 | print " ** Selection must be numeric" 175 | numeric_warning = False 176 | if range_warning: 177 | print " ** Selection must be in range" 178 | selection = raw_input(prompt + " [1 - %d]: " % len(choices)) 179 | try: 180 | selection = int(selection) 181 | except ValueError: 182 | numeric_warning = True 183 | else: 184 | if selection >=1 and selection <= len(choices): 185 | return choices[selection-1] 186 | else: 187 | range_warning = True 188 | 189 | def option_select(choices, prompt="Which choice?"): 190 | option_warning = False 191 | prompt = prompt + " [" + "/".join(choices) + "]:" 192 | while True: 193 | if option_warning: 194 | print " ** Input must be one of the options." 195 | option_warning = False 196 | selection = raw_input(prompt) 197 | if selection in choices: 198 | return selection 199 | else: 200 | option_warning = True 201 | 202 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML>=3.09 2 | argparse>=1.2.1 3 | straight.plugin>=1.0 4 | -------------------------------------------------------------------------------- /scripts/pin: -------------------------------------------------------------------------------- 1 | __pincomplete() 2 | { 3 | local cur 4 | COMPREPLY=() 5 | cur="${COMP_WORDS[@]:1}" 6 | COMPREPLY=( $(__pincomp $COMP_CWORD ${cur}) ) 7 | return 0 8 | } 9 | complete -F __pincomplete pin -------------------------------------------------------------------------------- /settings.yml: -------------------------------------------------------------------------------- 1 | foo: 2 | - bar 3 | - baz 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | from pin import VERSION 6 | 7 | setup( 8 | name='pin', 9 | version="0.1rc1", 10 | packages=['pin', 'pin.plugins'], 11 | scripts=['bin/pin.sh', 'bin/__pin', 'bin/__pincomp'], 12 | install_requires=['PyYAML', 'argparse', 'straight.plugin'], 13 | provides=['pin'], 14 | author="Dustin Lacewell", 15 | author_email="dlacewell@gmail.com", 16 | url="https://github.com/dustinlacewell/pin", 17 | description="pin is a generic project management tool for the commandline.", 18 | long_description=open('README.markdown').read(), 19 | ) 20 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import os, shutil 2 | 3 | 4 | from yaml import load, dump 5 | try: 6 | from yaml import CLoader as Loader 7 | from yaml import CDumper as Dumper 8 | except ImportError: 9 | from yaml import Loader, Dumper 10 | 11 | 12 | import pin 13 | pin.SETTINGS_ROOT = '/tmp/' 14 | from pin import registry, util, config 15 | 16 | def _destroy_settings(): 17 | path = os.path.join(pin.SETTINGS_ROOT, pin.SETTINGS_FOLDER) 18 | shutil.rmtree(path) 19 | 20 | 21 | def test_settings_merge(): 22 | a = {'1':'a', '2':'b', '3':[1,2,3]} 23 | b = {'1':'a', '3':[4,5,6]} 24 | c = config.merge(b, a) 25 | assert c['3'] == [4,5,6] 26 | assert c['2'] == 'b' 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/test_events.py: -------------------------------------------------------------------------------- 1 | import os, shutil 2 | 3 | 4 | from yaml import load, dump 5 | try: 6 | from yaml import CLoader as Loader 7 | from yaml import CDumper as Dumper 8 | except ImportError: 9 | from yaml import Loader, Dumper 10 | 11 | 12 | import pin 13 | pin.SETTINGS_ROOT = '/tmp/' 14 | from pin import registry, util, event 15 | 16 | def _destroy_settings(): 17 | path = os.path.join(pin.SETTINGS_ROOT, pin.SETTINGS_FOLDER) 18 | shutil.rmtree(path) 19 | 20 | CALLED = False 21 | 22 | def _callback(*args, **kwargs): 23 | global CALLED 24 | CALLED = True 25 | 26 | def test_event_register(): 27 | event.register('test-cb', _callback) 28 | callbacks = event.get_events() 29 | assert 'test-cb' in callbacks 30 | assert _callback in callbacks['test-cb'] 31 | 32 | def test_event_fire(): 33 | global CALLED 34 | CALLED = False 35 | event.fire('test-cb') 36 | assert CALLED == True 37 | 38 | def test_event_unregister(): 39 | event.unregister('test-cb', _callback) 40 | callbacks = event.get_events() 41 | assert _callback not in callbacks['test-cb'] 42 | -------------------------------------------------------------------------------- /tests/test_registry.py: -------------------------------------------------------------------------------- 1 | import os, shutil 2 | 3 | 4 | from yaml import load, dump 5 | try: 6 | from yaml import CLoader as Loader 7 | from yaml import CDumper as Dumper 8 | except ImportError: 9 | from yaml import Loader, Dumper 10 | 11 | 12 | import pin 13 | pin.SETTINGS_ROOT = '/tmp/' 14 | from pin import registry, util 15 | 16 | 17 | def _destroy_settings(): 18 | path = os.path.join(pin.SETTINGS_ROOT, pin.SETTINGS_FOLDER) 19 | shutil.rmtree(path) 20 | 21 | def test_settings_testing_path(): 22 | assert util.get_settings_path().startswith('/tmp') 23 | 24 | def test_create_settings_folder(): 25 | _destroy_settings() 26 | registry.establish_settings_folder() 27 | print "Checking '%s' exists..." % util.get_settings_path() 28 | assert os.path.exists(util.get_settings_path()) 29 | print "Checking '%s' exists..." % util.get_settings_filename() 30 | assert os.path.isfile(util.get_settings_filename()) 31 | print "Checking '%s' exists..." % util.get_registry_filename() 32 | assert os.path.isfile(util.get_registry_filename()) 33 | 34 | def test_create_registry(): 35 | with open(util.get_registry_filename(), 'r') as file: 36 | _registry = load(file, Loader=Loader) or default 37 | assert 'projects' in _registry 38 | _projects = _registry['projects'] 39 | assert isinstance(_projects, dict) 40 | 41 | def test_load_registry(): 42 | registry.load_registry() 43 | _projects = registry.get_registry() 44 | assert len(_projects) == 0 45 | assert isinstance(_projects, dict) 46 | 47 | def test_register_path(): 48 | registry.register('/tmp') 49 | assert '/tmp' in registry.get_registry() 50 | 51 | def test_raw_pathfor(): 52 | assert registry.pathfor('tmp') == '/tmp' 53 | 54 | def test_unregister_path(): 55 | registry.unregister('/tmp') 56 | assert '/tmp' not in registry.get_registry() 57 | 58 | def test_register_alias_path(): 59 | registry.register('/tmp', 'foobar') 60 | projects = registry.get_registry() 61 | assert '/tmp' in projects 62 | assert projects['/tmp']['alias'] == 'foobar' 63 | aliases = registry.get_aliases() 64 | assert 'foobar' in aliases 65 | assert aliases['foobar'] == '/tmp' 66 | 67 | def test_unregister_alias_path(): 68 | registry.unregister('/tmp') 69 | assert '/tmp' not in registry.get_registry() 70 | assert 'foobar' not in registry.get_aliases() 71 | 72 | def test_register_save(): 73 | registry.register('/tmp') 74 | registry.save_registry() 75 | with open(util.get_registry_filename(), 'r') as file: 76 | _registry = load(file, Loader=Loader) or default 77 | assert 'projects' in _registry 78 | _projects = _registry['projects'] 79 | assert isinstance(_projects, dict) 80 | assert '/tmp' in _projects 81 | registry.unregister('/tmp') 82 | 83 | def test_register_alias_save(): 84 | registry.register('/tmp', 'foobar') 85 | registry.save_registry() 86 | with open(util.get_registry_filename(), 'r') as file: 87 | _registry = load(file, Loader=Loader) or default 88 | assert 'projects' in _registry 89 | _projects = _registry['projects'] 90 | assert isinstance(_projects, dict) 91 | assert '/tmp' in _projects 92 | assert 'alias' in _projects['/tmp'] 93 | assert 'foobar' == _projects['/tmp']['alias'] 94 | 95 | def test_name_is_registered(): 96 | assert registry.name_is_registered('tmp') 97 | assert registry.name_is_registered('foobar') 98 | 99 | def test_path_is_registered(): 100 | assert registry.path_is_registered('/tmp') 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os, shutil 2 | 3 | 4 | from yaml import load, dump 5 | try: 6 | from yaml import CLoader as Loader 7 | from yaml import CDumper as Dumper 8 | except ImportError: 9 | from yaml import Loader, Dumper 10 | 11 | 12 | import pin 13 | pin.SETTINGS_ROOT = '/tmp/' 14 | from pin import registry, util, config 15 | 16 | def _destroy_settings(): 17 | path = os.path.join(pin.SETTINGS_ROOT, pin.SETTINGS_FOLDER) 18 | shutil.rmtree(path) 19 | --------------------------------------------------------------------------------