Skip to content

Commit

Permalink
Add windows registry and environment variable support
Browse files Browse the repository at this point in the history
Environment variables are accessed using the registry and supports
adding and removing environment variables using the registry.
  • Loading branch information
MHendricks committed Mar 4, 2023
1 parent bef6ff2 commit 812ecc4
Show file tree
Hide file tree
Showing 9 changed files with 872 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/python-static-analysis-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ jobs:
python: ['3.7', '3.8', '3.9', '3.10']

runs-on: 'windows-latest'
# Enable registry write testing
env:
CASEMENT_TEST_WRITE_ENV: 1

steps:
- name: Checkout code
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ A Python library that provides useful functionality for managing Microsoft Windo
* Finding, creating and moving shortcuts.
* Pinning shortcuts to the taskbar and start menu.
* Flashing a window to get a users attention.
* Windows Registry reading and modification.
* Windows environment variable interface via registry allowing for permanent modification.

## Shortcut management

Expand Down Expand Up @@ -43,6 +45,54 @@ Pinning/unpinning a shortcut to the taskbar:
C:\blur\dev\casement>casement shortcut unpin "C:\Users\Public\Desktop\My Shortcut.lnk" -t
```

# Environment Variables

Access and modify environment variables stored in the registry using
`casement.env_var.EnvVar`. This lets you access the raw un-expanded file registry
values unlike what you see with os.environ. You can permanently modify the
environment variables for the system.
```python
from casement.env_var import EnvVar

user_env = EnvVar(system=False)
user_env['VARIABLE_A'] = 'C:\\PROGRA~1'
expanded = user_env.normalize_path(user_env['VARIABLE_A'], tilde=False)
assert expanded == 'C:\\Program Files'
if 'VARIABLE_A' in user_env:
del user_env['VARIABLE_A']
```

# Windows Registry

Read and modify registry keys with `casement.registry.RegKey`.
```py
from casement.registry import RegKey

reg = RegKey('HKLM', 'Software\\Autodesk\\Maya')
for ver in reg.child_names():
# Attempt to access a specific sub-key that exists if ver is an version key
child = reg.child('{}\\Setup\\InstallPath'.format(ver))
if child.exists():
print(ver, child.entry('MAYA_INSTALL_LOCATION').value())
```

There is a map of common registry locations like environment variables and classes.

```python
from casement.registry import RegKey, REG_LOCATIONS

# Access to the user and system keys driving `HKEY_CLASSES_ROOT`
classes_system = RegKey(*REG_LOCATIONS['system']['classes'])
classes_user = RegKey(*REG_LOCATIONS['user']['classes'])

# Access to the user and system key controlling persistent Environment Variables
env_var_system = RegKey(*REG_LOCATIONS['system']['env_var'])
env_var_user = RegKey(*REG_LOCATIONS['user']['env_var'])

# Uninstall key for "Add/Remove Programs"
uninstall_system = RegKey(*REG_LOCATIONS['system']['uninstall'])
```

# Installing

Casement can be installed by the standard pip command `pip install casement`.
Expand Down
154 changes: 154 additions & 0 deletions casement/env_var.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import logging
import os
from contextlib import contextmanager

import win32api
import win32con
import win32file
import win32gui
from six.moves import winreg
from six.moves.collections_abc import MutableMapping

from .registry import REG_LOCATIONS, RegKey

_logger_modify = logging.getLogger(__name__ + ".modify")
_logger_broadcast = logging.getLogger(__name__ + ".broadcast")


class EnvVar(MutableMapping):
"""A dictionary like mapping of environment variables as defined in the
registry. Stored environment variables are not expanded by default. You can
use `normalize_path` for various path manipulation like expanding variables
and dealing with short/long file paths.
Arguments:
system (bool, optional): Controls if this instance modifies system or
user environment variables.
Every time you set or delete a environment variable it will send the
`WM_SETTINGCHANGE` message notifying other applications that system-wide
settings have changed.
See: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-settingchange
You can prevent this message from being sent every time you modify env vars
by using the `EnvVar.delayed_broadcast()` with context:
user_env = EnvVar(system=False)
with EnvVar.delayed_broadcast():
user_env['VAR_B'] = 'value b'
user_env['VAR_C'] = 'value c'
del user_env['VAR_A']
By using `EnvVar.delayed_broadcast()` broadcast will only be called once,
instead of three times.
"""

# These class variables are used to control if broadcast sends its message
_broadcast_enabled = True
_broadcast_required = False

def __init__(self, system=False):
key, sub_key = REG_LOCATIONS["system" if system else "user"]['env_var']
self.__reg__ = RegKey(key, sub_key)

def __delitem__(self, key):
entry = self.__reg__.entry(key)
with self.delayed_broadcast():
_logger_modify.debug('Deleting env var: "{}"'.format(key))
entry.delete()
type(self)._broadcast_required = True

def __getitem__(self, key):
entry = self.__reg__.entry(key)
try:
return entry.value()
except OSError:
raise KeyError(key)

def __iter__(self):
return iter(self.__reg__.entry_names())

def __len__(self):
reg_key = self.__reg__._key()
if not reg_key:
return 0

_, count, _ = winreg.QueryInfoKey(reg_key)
return count

def __setitem__(self, key, value, value_type=winreg.REG_EXPAND_SZ):
entry = self.__reg__.entry(key)
with self.delayed_broadcast():
_logger_modify.debug('Setting env var: "{}" to "{}"'.format(key, value))
entry.set(value, value_type=value_type)
type(self)._broadcast_required = True

@classmethod
def broadcast(cls):
"""Notify the system about the changes. Uses SendMessageTimeout to avoid
hanging if applications fail to respond to the broadcast.
"""
if cls._broadcast_enabled is False:
_logger_broadcast.debug(
'Skipping broadcasting that the environment was changed'
)
return

_logger_broadcast.debug('Broadcasting that the environment was changed')
win32gui.SendMessageTimeout(
win32con.HWND_BROADCAST,
win32con.WM_SETTINGCHANGE,
0,
'Environment',
win32con.SMTO_ABORTIFHUNG,
1000,
)
# Reset the needs broadcast variable now that we have broadcast
cls._broadcast_required = False

@classmethod
@contextmanager
def delayed_broadcast(cls):
"""Context manager that disables broadcast calls for each set/delete of
environment variables. Only if any set/delete calls were made inside the
with context, will it call broadcast.
"""
try:
current = cls._broadcast_enabled
cls._broadcast_enabled = False
yield
finally:
cls._broadcast_enabled = current
if cls._broadcast_required:
cls.broadcast()

@classmethod
def normalize_path(cls, path, expandvars=True, tilde=None, normpath=True):
"""Normalize a path handling short/long names, env vars and normpath.
Args:
path (str): Input path to expand.
expandvars (bool, optional): Expand environment variables.
tilde (bool, optional): Converts windows short paths(`C:\\PROGRA~1`).
If True, short paths are expanded to full file paths. If False
file paths are converted to short paths. Nothing is done if None.
normpath (bool, optional): If True calls os.path.normpath.
Returns:
str: Input path with any tilde-shortenings expanded.
Raises:
pywintypes.error: Raised if path doesn't exist when tilde is not None.
"""
if tilde:
path = win32api.GetShortPathName(path)
elif tilde is False:
path = win32file.GetLongPathName(path)

if expandvars:
path = os.path.expandvars(path)

if normpath:
path = os.path.normpath(path)

return path
Loading

1 comment on commit 812ecc4

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.