Skip to content

Commit

Permalink
Version update - 2021.1.6
Browse files Browse the repository at this point in the history
* Added a new `PythonContextMenuItem` class inheriting `ContextMenuItem` which takes python_function and converts it to an executable command.

* Added type hints and argument documentation for most of the methods.

* Added `.create_for`, `.delete` and `.delete_for` methods for `ContextMenuItem` and `ContextMenuGroup`.

* Updated usage example in __main__.

* Updated README.md to reflect the changes.
  • Loading branch information
naveennamani committed Jan 6, 2021
1 parent e757146 commit 8ecdb88
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 28 deletions.
66 changes: 57 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ You can download the pywin_contextmenu.py file from this repository (https://raw

Or you can install this package from pypi using
```shell script
pip install pywin_contextmenu
pip install pywin-contextmenu
```
and simply import in your scripts.
```python
import pywin_contextmenu as pycm
```

## Usage
## Detailed Usage
```python
import pywin_contextmenu as pycm

Expand Down Expand Up @@ -56,6 +56,32 @@ pycm.ContextMenuGroup("Group 1", items = [
)
```

## Simple and easy usage
```python
import pywin_contextmenu as pycm

def test_function(file_or_dir_name):
print(file_or_dir_name)
input("Press ENTER to continue")


# create the nested groups to execute direct commands, python functions
# and python scripts
cmgroup = pycm.ContextMenuGroup("Group 1", items = [
pycm.ContextMenuItem("Open cmd", "cmd.exe"),
pycm.PythonContextMenuItem("Test py", test_function),
pycm.ContextMenuGroup("Group 2", items = [
pycm.PythonContextMenuItem("Python script test", pycm.python_script_cmd(
"example.py", rel_path = True, hide_terminal = True
))
])
])

# create the group for the current user to be shown on right click of
# directory, and all files
cmgroup.create_for(pycm.UserType.CURR_USER, [pycm.RootType.DIr, pycm.RootType.ALL_FILES])
```

## API
The script depends on two main classes `ContextMenuItem` and `ContextMenuGroup`.

Expand All @@ -72,10 +98,29 @@ ContextMenuItem(
extended = False # set to True if the item is to be shown when right clicked with shift button
)
```
For creating the item simply call the `.create` method.
```
ContextMenuItem.create(root_key) # Create the item at the given registry root_key
#### Methods
`.create(root_key: HKEYType)` - Adds the item to the registry at the given registry `root_key`. Obtain the `root_key` using `get_root` utility method.

`.create_for(user_type: UserType, root_type: List[RootType])` - Adds the items for given user and given root locations.

`.delete(root_key: HKEYType)` - Delete the item at the given `root_key`.

`.delete_for(user_type: UserType, root_type: List[RootType])` - Delete the items for the given user and the root locations.
###### Note: The `.delete` is not preferred as any `root_key` can be passed. Instead please use `.delete_for` method as the registry keys to be deleted will be automatically and safely deleted.

### `PythonContextMenuItem`
This class inherits `ContextMenuItem` and converts a python function to an executable command.
Simply pass the python function as `python_function` argument.
```python
PythonContextMenuItem(
item_name,
python_function: Callable[[str], Any], # callable python function which should take file/folder name as the single argument
item_reg_key = "",
icon = "",
extended = False
)
```

### `ContextMenuGroup`
This class groups multiple items and subgroups.
```python
Expand All @@ -87,6 +132,8 @@ ContextMenuGroup(
items = [] # items to be displayed on this group
)
```

#### Methods
For adding items or groups to a group instance call `add_item`/`add_items` method of the class.
```python
ContextMenuGroup.add_item(item)
Expand All @@ -100,12 +147,13 @@ Then to create the group and add to the contextmenu simply call `.create` with t
```
ContextMenuGroup.create(root_key) # Create the group and add to contextmenu
```
The class also has method `.create`, `.create_for`, `.delete` and `.delete_for` methods which are same as that of the methods of `ContextMenuItem`.

#### Note: All methods of `ContextMenuItem` and `ContextMenuGroup` returns `self`, so they can be chained. Adding items to `ContextMenuGroup` will not add them to the contextmenu/registry unless `.create` method is called.
## Utility methods available

* `RootType` - an `Enum` for chosing where the context menu item/group will be displayed
* `UserType` - an `Enum` for chosing whether to add the context menu for current user or for all users
## Utility methods available
* `RootType` - an `Enum` for choosing where the context menu item/group will be displayed
* `UserType` - an `Enum` for choosing whether to add the context menu for current user or for all users
* `get_root(user_type: UserType, root_type: RootType, file_type: str)` - creates/opens the registry key for the selected user_type and root_type.
If the `root_type` is `RootType.FILE` then `file_type` argument is required and indicates the file extention.
* `python_script_cmd(script_path, rel_path = False, hide_terminal = False)` - a utility function to convert a given `script_path` to an executable command.
Expand All @@ -116,4 +164,4 @@ ContextMenuGroup.create(root_key) # Create the group and add to contextmenu
* [ ] Add a way to handle passing of multiple files/folders to the selected script without launching multiple instances of the script.

---
# © Naveen Namani
# © Naveen Namani
143 changes: 125 additions & 18 deletions pywin_contextmenu.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# coding=utf-8
# (c) Naveen Namani
# https://github.com/naveennamani
# __version__ = 2020.9.7
# https://github.com/naveennamani/pywin_contextmenu
# __version__ = 2021.1.6
import os
import sys
import warnings
from enum import Enum
from inspect import isfunction
from pathlib import Path
from typing import Callable
from typing import List
from typing import Union
from winreg import CloseKey
from winreg import CreateKey
from winreg import DeleteKey
Expand All @@ -19,7 +24,7 @@
from winreg import SetValue
from winreg import SetValueEx

__version__ = (2020, 9, 7)
__version__ = (2021, 1, 6)


################################################################################
Expand Down Expand Up @@ -50,15 +55,27 @@ class CyclicGroupException(Exception):
# classes for storing the information about ContextMenuItem and ContextMenuGroup
################################################################################
class ContextMenuItem(object):
def __init__(self, item_name, command, item_reg_key = "", icon = "",
extended = False):
def __init__(
self,
item_name: str, # to be displayed in the contextmenu
command: str, # command to be executed
item_reg_key: str = "", # key to be used in the registry
icon: str = "", # path to the icon to be shown along with item
extended: bool = False # if true, item will be shown with shift key
):
self.item_name = item_name
self.item_reg_key = item_name if item_reg_key == "" else item_reg_key
self.icon = icon
self.command = command
self.extended = extended

def create(self, root_key):
def create(self, root_key: HKEYType):
"""
Adds entries into the registry for creating the item
:param root_key: HKEYType key where the item is to be created.
Obtain the key using `get_root` function
:return: Returns the ContextMenuItem item
"""
item_key = CreateKey(root_key, self.item_reg_key)
SetValue(item_key, "", REG_SZ, self.item_name)
SetValue(item_key, "command", REG_SZ, self.command)
Expand All @@ -69,10 +86,64 @@ def create(self, root_key):
CloseKey(item_key)
return self

def create_for(self, user_type: UserType, root_types: List[RootType]):
"""
Add the item to multiple locations
Usage: contextmenu_item.create_for(UserType.CURR_USER, [RootType.DIR, RootType.DIR_BG])
"""
for root_type in root_types:
self.create(get_root(user_type, root_type))
return self

def delete(self, root_key: HKEYType):
""" Delete the registry key at `root_key` """
delete_item(root_key, self.item_reg_key)
return self

def delete_for(self, user_type: UserType, root_types: List[RootType]):
for root_type in root_types:
self.delete(get_root(user_type, root_type))
return self


class PythonContextMenuItem(ContextMenuItem):
def __init__(
self,
item_name: str, # to be displayed in the contextmenu
python_function: Callable, # python function to be executed
item_reg_key: str = "", # key to be used in the registry
icon: str = "", # path to the icon to be shown along with item
hide_terminal: bool = False, # hide the python console on execution
extended: bool = False # if true, item will be shown with shift key
):
if isfunction(python_function):
script_path = Path(python_function.__code__.co_filename)
script_folder = script_path.parent
script_name = script_path.name.split(script_path.suffix)[0]
function_name = python_function.__name__
py_script = f"""import os, sys; os.chdir(r'{script_folder}'); __import__('{script_name}').{function_name}(sys.argv[1])"""
py_executable = sys.executable.replace(
"python.exe",
"pythonw.exe") if hide_terminal else sys.executable
command = f"""{py_executable} -c "{py_script}" "%V" """
print(command)
else:
raise TypeError(
"Please pass a function type to python_function argument")
super(PythonContextMenuItem, self).__init__(
item_name, command, item_reg_key, icon, extended)


class ContextMenuGroup(object):
def __init__(self, group_name, group_reg_key = "", icon = "",
extended = False, items = None):
def __init__(
self,
group_name: str, # to be displayed in the contextmenu
group_reg_key: str = "", # key to be used in the registry
icon: str = "", # path to the icon to be shown along with item
items: "List[Union[ContextMenuItem, ContextMenuGroup]]" = None,
# items and subgroups to be shown
extended: bool = False # if true, item will be shown with shift key
):
self.group_name = group_name
self.group_reg_key = group_name if group_reg_key == "" else group_reg_key
self.icon = icon
Expand All @@ -81,21 +152,28 @@ def __init__(self, group_name, group_reg_key = "", icon = "",
if items is not None:
self.add_items(items)

def add_item(self, item):
def add_item(self, item: "Union[ContextMenuItem, ContextMenuGroup]"):
assert isinstance(
item, (ContextMenuItem, ContextMenuGroup)
), "Please pass instance of ContextMenuItem or ContextMenuGroup"
self.items.append(item)
return self

def add_items(self, items):
def add_items(self,
items: "List[Union[ContextMenuItem, ContextMenuGroup]]"):
assert isinstance(items, (
tuple, list)), "Please provide an instance of tuple or list"
for item in items:
self.add_item(item)
return self

def create(self, root_key):
def create(self, root_key: HKEYType):
"""
Adds entries into the registry for creating the item
:param root_key: HKEYType key where the item is to be created.
Obtain the key using `get_root` function
:return: Returns the ContextMenuGroup item
"""
if is_cyclic_group(self):
raise CyclicGroupException(
"Congratulations! You're about to break your registry")
Expand All @@ -116,6 +194,20 @@ def create(self, root_key):
CloseKey(group_key)
return self

def create_for(self, user_type: UserType, root_types: List[RootType]):
for root_type in root_types:
self.create(get_root(user_type, root_type))
return self

def delete(self, root_key: HKEYType):
delete_item(root_key, self.group_reg_key)
return self

def delete_for(self, user_type: UserType, root_types: List[RootType]):
for root_type in root_types:
self.delete(get_root(user_type, root_type))
return self


################################################################################
# Utility functions
Expand Down Expand Up @@ -176,7 +268,7 @@ def is_cyclic_group(group: ContextMenuGroup, all_group_reg_keys = None) -> bool:
return False


def _del_key(rk):
def _del_key(rk: HKEYType):
no_subkeys, _, __ = QueryInfoKey(rk)
# print(no_subkeys)
for i in range(no_subkeys):
Expand All @@ -186,7 +278,7 @@ def _del_key(rk):
DeleteKey(rk, "")


def delete_item(root_key, item_reg_key):
def delete_item(root_key: HKEYType, item_reg_key: str):
try:
_del_key(OpenKey(root_key, item_reg_key))
except FileNotFoundError:
Expand All @@ -197,27 +289,42 @@ def delete_item(root_key, item_reg_key):
print_exc()


def test_function(path_or_file_name):
print("Path/file named selected", path_or_file_name)
input("Press ENTER to continue")


if __name__ == '__main__':
# add a menu for current user to be shown in directory background contextmenu
user_root = get_root(UserType.CURR_USER, RootType.DIR_BG)

# beautiful pythonic way
cmgroup = ContextMenuGroup("Group 1", items = [
# define complex nested contextmenu groups in a beautiful pythonic way
cmgroup = ContextMenuGroup("Group 1", items = [ # first group
ContextMenuItem("Item 1", "cmd.exe"),
ContextMenuItem("Item 2", "cmd.exe"),
ContextMenuGroup("Group 2", items = [
PythonContextMenuItem("Python fn", test_function),
# execute a function function
ContextMenuGroup("Group 2", items = [ # second group
ContextMenuItem("Item 3", "cmd.exe"),
ContextMenuItem("Item 4", "cmd.exe"),
ContextMenuGroup("Group 3", items = [
ContextMenuGroup("Group 3", items = [ # one more nested group
ContextMenuItem("Item 5", "cmd.exe"),
ContextMenuItem("Item 6", "cmd.exe")
])
])
]).create(user_root)

# create the group to be shown at multiple locations easily
cmgroup.create_for(UserType.CURR_USER, [RootType.DIR, RootType.ALL_FILES])
input("Group 1 created, press ENTER to continue")

# delete the menu created for DIB_BG location
delete_item(user_root, cmgroup.group_reg_key)
input("Group 1 deleted, press ENTER to close")

# delete for other locations
cmgroup.delete_for(UserType.CURR_USER, [RootType.DIR])

# convert a python script to executable command using python_script_cmd function
cmgroup = ContextMenuGroup("Group 1", items = [
ContextMenuItem("Item 1", python_script_cmd(
"example.py", rel_path = True)),
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name = 'pywin_contextmenu',
version = '2020.9.7',
version = '2021.1.6',
description = 'A simple and intuitive way to add your custom scripts to '
'the windows right click contextmenu.',
long_description = long_description,
Expand Down

0 comments on commit 8ecdb88

Please sign in to comment.