Skip to content

Commit 13bc70f

Browse files
authored
Merge pull request #34 from homebysix/1.3.0-dev
1.3.0 merge to main
2 parents 97e51cb + 42f5430 commit 13bc70f

7 files changed

Lines changed: 217 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,40 @@ All notable changes to this project will be documented in this file. This projec
55

66
## [Unreleased]
77

8-
Nothing yet.
8+
The focus of this release is to make docklib functions less focused on dock item labels, since labels can change depending on the user's selected language. (See [#32](https://github.com/homebysix/docklib/issues/32) for details.)
9+
10+
### Added
11+
12+
- A new `findExistingEntry` function that can find dock items based on several attributes.
13+
14+
The default behavior of `findExistingEntry` is to match the provided string on (in order of preference):
15+
16+
1. label
17+
2. path
18+
3. filename with extension
19+
4. filename without extension
20+
21+
The `match_on` parameter can be specified to select only one of those attributes to match, if desired. See the `findExistingEntry` function docstring for available parameters and values.
22+
23+
### Changed
24+
25+
- The `findExistingLabel` function is now simply a pointer to the new `findExistingEntry` function. `findExistingLabel` will be maintained for backward-compatibility.
26+
27+
- The `removeDockEntry` function has a new `match_on` parameter that mirrors the same parameter in `findExistingEntry`. Default behavior is to match on the same attributes listed above, in the same order of preference. (This is a change in behavior from previous versions of docklib. If you prefer to continue removing items solely based on label, you should specify `match_on="label"` in your function call.)
28+
29+
- The `replaceDockEntry` function has two new parameters:
30+
- `match_str`, which allows specifying the item intended to be replaced in the dock (replaces the now deprecated `label` parameter).
31+
- `match_on`, which mirrors the same parameter in `findExistingEntry`. Default behavior is to match on the same attributes listed above, in the same order of preference.
32+
33+
### Deprecated
34+
35+
- The `label` parameter of `replaceDockEntry` is deprecated, and it's encouraged to use `match_str` instead. This allows existing items to be replaced based on multiple attributes rather than just label. As stated above, this makes dock customization scripts more reliable in multilingual environments.
36+
37+
A warning has been added that alerts administrators to this deprecation.
38+
39+
- **This is the last release of docklib that will support Python 2.** Future releases will only be tested in Python 3.
40+
41+
If you haven't started bundling a Python 3 runtime for your management tools, [this blog article from @scriptingosx](https://scriptingosx.com/2020/02/wrangling-pythons/) is a good read. Also: a reminder that docklib is already included in the "recommended" flavor of the [macadmins/python](https://github.com/macadmins/python) packages.
942

1043
## [1.2.1] - 2021-03-01
1144

@@ -15,7 +48,7 @@ Nothing yet.
1548

1649
### Fixed
1750

18-
- Fixed issue preventing `findExistingLabel()` from finding URLs' labels
51+
- Fixed issue preventing `findExistingLabel` from finding URLs' labels
1952

2053
## [1.2.0] - 2020-10-15
2154

@@ -25,7 +58,7 @@ Nothing yet.
2558

2659
- Published docklib to PyPI so that administrators can manage it with `pip` and more easily bundle it in [custom Python frameworks](https://github.com/macadmins/python). Adjusted repo file structure to match Python packaging standards.
2760
- Created __build_pkg.sh__ script for creation of macOS package installer.
28-
- Added `findExistingURL()` and `removeDockURLEntry()` functions for handling URL items.
61+
- Added `findExistingURL` and `removeDockURLEntry` functions for handling URL items.
2962
- Created a few basic unit tests.
3063

3164
### Changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ pip install docklib
3232

3333
This method is not intended to be used directly on managed devices, but it could be leveraged alongside a custom Python framework (like one built with [macadmins/python](https://github.com/macadmins/python) or [relocatable-python](https://github.com/gregneagle/relocatable-python)) using a requirements file.
3434

35+
### Managed Python
36+
37+
Docklib is included in the "recommended" flavor of the [macadmins/python](https://github.com/macadmins/python) release package. Installing this package and using `#!/usr/local/managed_python3` for your docklib script shebang may be the most self-contained and future-proof way to deploy docklib.
38+
3539
### Manual
3640

3741
Another method of using docklib is to simply place the docklib.py file in the same location as the Python script(s) you use to manipulate the macOS dock. Some examples of such scripts are included below.
@@ -109,7 +113,7 @@ import os
109113
from docklib import Dock
110114

111115
dock = Dock()
112-
if dock.findExistingLabel("Documents", section="persistent-others") == -1:
116+
if dock.findExistingEntry("Documents", section="persistent-others") == -1:
113117
item = dock.makeDockOtherEntry(
114118
os.path.expanduser("~/Documents"), arrangement=3, displayas=1, showas=1
115119
)
@@ -125,7 +129,7 @@ import os
125129
from docklib import Dock
126130

127131
dock = Dock()
128-
if dock.findExistingLabel("GitHub", section="persistent-others") == -1:
132+
if dock.findExistingEntry("GitHub", section="persistent-others") == -1:
129133
item = dock.makeDockOtherURLEntry("https://www.github.com/", label="GitHub")
130134
dock.items["persistent-others"] = [item] + dock.items["persistent-others"]
131135
dock.save()

RELEASING.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
1. Run docklib unit tests and fix any errors:
1919

20-
/usr/local/munki/munki-python -m unittest -v tests/unit.py
20+
managed_python3 -m unittest -v tests/unit.py
2121

2222
1. Build a new distribution package:
2323

@@ -30,9 +30,9 @@
3030

3131
1. View resulting project on test.pypi.org and make sure it looks good.
3232

33-
1. Install test docklib in Munki Python on a test Mac:
33+
1. Install test docklib in MacAdmins Python on a test Mac:
3434

35-
/usr/local/munki/Python.framework/Versions/Current/bin/python3 -m pip install --upgrade -i https://test.pypi.org/simple/ docklib
35+
managed_python3 -m pip install --upgrade -i https://test.pypi.org/simple/ docklib
3636

3737
1. Perform tests - manual for now.
3838

@@ -42,9 +42,9 @@
4242

4343
1. View resulting project on pypi.org and make sure it looks good.
4444

45-
1. Install production docklib in Munki Python on a test Mac:
45+
1. Install production docklib in MacAdmins Python on a test Mac:
4646

47-
/usr/local/munki/Python.framework/Versions/Current/bin/python3 -m pip install --upgrade docklib
47+
managed_python3 -m pip install --upgrade docklib
4848

4949
1. Build new installer package using __build_pkg.sh__:
5050

docklib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .docklib import *
22

3-
__version__ = "1.2.1"
3+
__version__ = "1.3.0"

docklib/docklib.py

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,26 @@
22

33
# pylint: disable=C0103
44

5-
"""Python module intended to assist IT administrators with manipulation of the macOS Dock."""
5+
"""Python module intended to assist IT administrators with manipulation of the macOS Dock.
6+
7+
See project details on GitHub: https://github.com/homebysix/docklib
8+
"""
69

710
import os
811
import subprocess
912
from distutils.version import LooseVersion
1013
from platform import mac_ver
1114

15+
try:
16+
# Python 3
17+
from urllib.parse import unquote, urlparse
18+
except ImportError:
19+
# Python 2
20+
from urllib import unquote
21+
22+
from urlparse import urlparse
23+
24+
1225
# pylint: disable=E0611
1326
from Foundation import (
1427
NSURL,
@@ -17,7 +30,6 @@
1730
CFPreferencesSetAppValue,
1831
)
1932

20-
2133
# pylint: enable=E0611
2234

2335

@@ -34,6 +46,7 @@ class Dock:
3446
_DOCK_PLIST = os.path.expanduser("~/Library/Preferences/com.apple.dock.plist")
3547
_DOCK_LAUNCHAGENT_ID = "com.apple.Dock.agent"
3648
_DOCK_LAUNCHAGENT_FILE = "/System/Library/LaunchAgents/com.apple.Dock.plist"
49+
# TODO: static-apps and static-others
3750
_SECTIONS = ["persistent-apps", "persistent-others"]
3851
_MUTABLE_KEYS = [
3952
"autohide",
@@ -97,6 +110,8 @@ def save(self):
97110
except Exception:
98111
raise DockError
99112
for key in self._MUTABLE_KEYS:
113+
# Python doesn't support hyphens in attribute names, so convert
114+
# to/from underscores as needed.
100115
if getattr(self, key.replace("-", "_")) is not None:
101116
try:
102117
CFPreferencesSetAppValue(
@@ -113,39 +128,82 @@ def save(self):
113128
subprocess.call(["/bin/launchctl", "load", self._DOCK_LAUNCHAGENT_FILE])
114129
subprocess.call(["/bin/launchctl", "start", self._DOCK_LAUNCHAGENT_ID])
115130

116-
def findExistingLabel(self, test_label, section="persistent-apps"):
117-
"""Returns index of item with label matching test_label or -1 if not
118-
found."""
131+
def findExistingEntry(self, match_str, match_on="any", section="persistent-apps"):
132+
"""Returns index of a Dock item identified by match_str or -1 if no item
133+
is found.
134+
135+
match_on values:
136+
label: match dock items with this file-label or label
137+
(e.g. Safari)
138+
NOTE: Labels can vary depending on the user's selected language.
139+
path: match dock items with this path on disk
140+
(e.g. /System/Applications/Safari.app)
141+
name_ext: match dock items with this file/folder basename
142+
(e.g. Safari.app)
143+
name_noext: match dock items with this basename, without extension
144+
(e.g. Safari)
145+
any: Try all the criteria above in order, and return the first result.
146+
"""
119147
section_items = self.items[section]
120148
if section_items:
121-
# Most dock items use "file-label", but URLs use "label"
122-
for label_key in ("file-label", "label"):
149+
# Determine the order of attributes to match on.
150+
if match_on == "any":
151+
match_ons = ["label", "path", "name_ext", "name_noext"]
152+
else:
153+
match_ons = [match_on]
154+
155+
# Iterate through match criteria (ensures a full scan of the Dock
156+
# for each criterion, if matching on "any").
157+
for m in match_ons:
158+
# Iterate through items in section
123159
for index, item in enumerate(section_items):
124-
if item["tile-data"].get(label_key) == test_label:
160+
url = item["tile-data"].get("file-data", {}).get("_CFURLString", "")
161+
path = unquote(urlparse(url.rstrip("/")).path)
162+
name_ext = os.path.basename(path)
163+
name_noext = os.path.splitext(name_ext)[0]
164+
165+
if m == "label":
166+
# Most dock items use "file-label", but URLs use "label"
167+
for label_key in ("file-label", "label"):
168+
if item["tile-data"].get(label_key) == match_str:
169+
return index
170+
elif m == "path" and path == match_str:
171+
return index
172+
elif m == "name_ext" and name_ext == match_str:
173+
return index
174+
elif m == "name_noext" and name_noext == match_str:
125175
return index
126176

127177
return -1
128178

129-
def findExistingURL(self, test_url):
130-
"""Returns index of item with URL matching test_url or -1 if not
179+
def findExistingLabel(self, match_str, section="persistent-apps"):
180+
"""Points to findExistingEntry, maintained for compatibility."""
181+
return self.findExistingEntry(match_str, match_on="label", section=section)
182+
183+
def findExistingURL(self, match_url):
184+
"""Returns index of item with URL matching match_url or -1 if not
131185
found."""
132186
section_items = self.items["persistent-others"]
133187
if section_items:
134188
for index, item in enumerate(section_items):
135189
if item["tile-data"].get("url"):
136-
if item["tile-data"]["url"]["_CFURLString"] == test_url:
190+
if item["tile-data"]["url"]["_CFURLString"] == match_url:
137191
return index
138192

139193
return -1
140194

141-
def removeDockEntry(self, label, section=None):
142-
"""Removes a Dock entry with matching label, if any."""
195+
def removeDockEntry(self, match_str, match_on="any", section=None):
196+
"""Removes a Dock entry identified by "match_str", if any. Defaults to
197+
matching "match_str" by the "any" criteria order listed in the
198+
findExistingEntry docstring."""
143199
if section:
144200
sections = [section]
145201
else:
146202
sections = self._SECTIONS
147203
for sect in sections:
148-
found_index = self.findExistingLabel(label, section=sect)
204+
found_index = self.findExistingEntry(
205+
match_str, match_on=match_on, section=sect
206+
)
149207
if found_index > -1:
150208
del self.items[sect][found_index]
151209

@@ -155,22 +213,45 @@ def removeDockURLEntry(self, url):
155213
if found_index > -1:
156214
del self.items["persistent-others"][found_index]
157215

158-
def replaceDockEntry(self, thePath, label=None, section="persistent-apps"):
216+
def replaceDockEntry(
217+
self,
218+
newpath,
219+
match_str=None,
220+
match_on="any",
221+
label=None, # deprecated
222+
section="persistent-apps",
223+
):
159224
"""Replaces a Dock entry.
160225
161-
If label is None, then a label is derived from the item path.
162-
The new entry replaces an entry with the given or derived label
226+
If match_str is provided, it will be used to match the item to be replaced.
227+
See the findExistingEntry function docstring for possible "match_on" values.
228+
229+
If match_str is not provided, the item to be replaced will be derived from
230+
the newpath filename, without extension.
231+
232+
The "label" parameter is deprecated in favor of match_str and will be
233+
removed someday.
163234
"""
164235
if section == "persistent-apps":
165-
new_item = self.makeDockAppEntry(thePath)
236+
newitem = self.makeDockAppEntry(newpath)
166237
else:
167-
new_item = self.makeDockOtherEntry(thePath)
168-
if new_item:
169-
if not label:
170-
label = os.path.splitext(os.path.basename(thePath))[0]
171-
found_index = self.findExistingLabel(label, section=section)
172-
if found_index > -1:
173-
self.items[section][found_index] = new_item
238+
newitem = self.makeDockOtherEntry(newpath)
239+
if not newitem:
240+
return
241+
if label:
242+
print(
243+
"WARNING: The label parameter is deprecated. Use match_str instead. "
244+
"Details: https://github.com/homebysix/docklib/issues/32"
245+
)
246+
match_str = label
247+
match_on = "label"
248+
if not match_str:
249+
match_str = os.path.splitext(os.path.basename(newpath))[0]
250+
found_index = self.findExistingEntry(
251+
match_str, match_on=match_on, section=section
252+
)
253+
if found_index > -1:
254+
self.items[section][found_index] = newitem
174255

175256
def makeDockAppSpacer(self, type="spacer-tile"):
176257
"""Makes an empty space in the Dock."""

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)